diff --git a/eslint.config.mjs b/eslint.config.mjs index d5610a5..d3108a7 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,48 +1,15 @@ -import javascriptConfig from '@playcanvas/eslint-config/javascript'; -import prettierConfig from 'eslint-config-prettier/flat'; -import globals from 'globals'; +import typescriptConfig from '@playcanvas/eslint-config/typescript'; export default [ - ...javascriptConfig, + ...typescriptConfig, { - ignores: ['dist/**', 'types/**', 'test/fixtures/**'] + ignores: ['types/**', 'test/fixtures/**'] }, { - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.mocha, - ...globals.node - } - } - }, - { - rules: { - 'no-unused-expressions': 'off', - 'jsdoc/check-tag-names': 'off', - 'import/no-unresolved': 'off' - } - }, - { - files: ['**/*.mjs'], - languageOptions: { - globals: { - ...globals.node - } - }, + // chai assertions like expect(x).to.be.true are expression statements + files: ['test/**/*.test.js'], rules: { - 'import/no-named-as-default': 'off' + '@typescript-eslint/no-unused-expressions': 'off' } - }, - { - // Configuration specific to test files - files: ['**/*.test.js'], // Match test files - - rules: { - 'prefer-arrow-callback': 'off' // Allow function expressions in test files - } - }, - prettierConfig + } ]; diff --git a/package-lock.json b/package-lock.json index 7295889..ffecfb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,8 @@ "@rollup/plugin-node-resolve": "16.0.3", "@rollup/plugin-swc": "0.4.1", "@swc/core": "1.15.43", + "@types/node": "26.1.0", "chai": "5.3.3", - "eslint-config-prettier": "10.1.8", - "globals": "16.5.0", "mocha": "11.7.6", "playcanvas": "2.12.3", "prettier": "3.9.4", @@ -29,7 +28,7 @@ "rollup": "4.62.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/@altano/repository-tools": { @@ -1485,6 +1484,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -7019,9 +7028,10 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7071,6 +7081,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", diff --git a/package.json b/package.json index e173fc7..2e5de61 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,23 @@ { "name": "@playcanvas/attribute-parser", "version": "1.10.3", - "author": "PlayCanvas ", - "homepage": "https://github.com/playcanvas/attribute-parser#readme", "description": "The PlayCanvas Attribute Parser", "keywords": [ "playcanvas", "attribute", "parser" ], + "homepage": "https://github.com/playcanvas/attribute-parser#readme", + "bugs": { + "url": "https://github.com/playcanvas/attribute-parser/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/playcanvas/attribute-parser.git" + }, "license": "MIT", + "author": "PlayCanvas ", "type": "module", - "main": "dist/index.js", - "types": "types/index.d.ts", "exports": { ".": { "types": "./types/index.d.ts", @@ -22,41 +27,13 @@ }, "./libs": "./dist/libs.d.ts" }, - "bugs": { - "url": "https://github.com/playcanvas/attribute-parser/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/playcanvas/attribute-parser.git" - }, + "main": "dist/index.js", + "types": "types/index.d.ts", "files": [ - "dist/index.js", "dist/index.node.mjs", "dist/libs.d.ts", - "types", - "README.md", - "LICENSE.md" + "types" ], - "devDependencies": { - "@rollup/plugin-commonjs": "28.0.9", - "@rollup/plugin-node-resolve": "16.0.3", - "@rollup/plugin-swc": "0.4.1", - "@swc/core": "1.15.43", - "chai": "5.3.3", - "eslint-config-prettier": "10.1.8", - "globals": "16.5.0", - "mocha": "11.7.6", - "playcanvas": "2.12.3", - "prettier": "3.9.4", - "publint": "0.3.21", - "rollup": "4.62.2" - }, - "dependencies": { - "@playcanvas/eslint-config": "3.0.0-beta.8", - "@typescript/vfs": "^1.6.0", - "eslint": "^9.0.0", - "typescript": "^5.5.3" - }, "scripts": { "build": "rollup -c && npm run build:types", "build:types": "tsc -p tsconfig.build.json", @@ -69,7 +46,26 @@ "typecheck": "tsc --noEmit", "watch": "rollup -c -w" }, + "dependencies": { + "@playcanvas/eslint-config": "3.0.0-beta.8", + "@typescript/vfs": "^1.6.0", + "eslint": "^9.0.0", + "typescript": "^5.5.3" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "28.0.9", + "@rollup/plugin-node-resolve": "16.0.3", + "@rollup/plugin-swc": "0.4.1", + "@swc/core": "1.15.43", + "@types/node": "26.1.0", + "chai": "5.3.3", + "mocha": "11.7.6", + "playcanvas": "2.12.3", + "prettier": "3.9.4", + "publint": "0.3.21", + "rollup": "4.62.2" + }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } } diff --git a/rollup.config.mjs b/rollup.config.mjs index d6b0feb..e0b9e63 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -8,11 +8,15 @@ import ts from 'typescript'; const production = process.env.NODE_ENV === 'production'; -// Concatenates the TypeScript compiler options into a single file -const types = knownLibFilesForCompilerOptions({ target: ts.ScriptTarget.ES2023 }, ts).reduce((acc, file) => { - const src = fs.readFileSync(`node_modules/typescript/lib/${file}`, 'utf8'); - return `${acc}// ${file}\n${src}\n\n`; -}, ''); +// Concatenates the TypeScript compiler options into a single file. The known lib list can +// include files removed in newer TypeScript versions (e.g. lib.es2022.sharedmemory.d.ts), +// so skip any that no longer exist. +const types = knownLibFilesForCompilerOptions({ target: ts.ScriptTarget.ES2023 }, ts) + .filter((file) => fs.existsSync(`node_modules/typescript/lib/${file}`)) + .reduce((acc, file) => { + const src = fs.readFileSync(`node_modules/typescript/lib/${file}`, 'utf8'); + return `${acc}// ${file}\n${src}\n\n`; + }, ''); fs.mkdirSync('dist', { recursive: true }); fs.writeFileSync('dist/libs.d.ts', types); @@ -29,7 +33,7 @@ const swcPlugin = () => // Browser build: bundles typescript and @typescript/vfs and stubs out node built-ins, // for in-browser consumers (e.g. the editor), which supply lib types via a CDN. const browser = { - input: 'src/index.js', + input: 'src/index.ts', output: { dir: 'dist', format: 'esm' @@ -42,7 +46,8 @@ const browser = { }), resolve({ browser: true, // This instructs the plugin to resolve for the browser - preferBuiltins: false // Prefer the local versions of built-ins instead of Node.js versions + preferBuiltins: false, // Prefer the local versions of built-ins instead of Node.js versions + extensions: ['.js', '.ts'] }), swcPlugin() ] @@ -52,7 +57,7 @@ const browser = { // (real os/fs/path and a native require). The browser build stubs these out, which breaks // createDefaultMapFromNodeModules and TypeScript's getNodeSystem() under node. const node = { - input: 'src/index.js', + input: 'src/index.ts', external: [/^node:/, 'typescript', '@typescript/vfs'], output: { dir: 'dist', @@ -62,7 +67,8 @@ const node = { plugins: [ resolve({ exportConditions: ['node'], - preferBuiltins: true + preferBuiltins: true, + extensions: ['.js', '.ts'] }), swcPlugin() ] diff --git a/src/index.js b/src/index.ts similarity index 67% rename from src/index.js rename to src/index.ts index f9f23b5..dadfdf5 100644 --- a/src/index.js +++ b/src/index.ts @@ -1,21 +1,45 @@ import { createSystem, createDefaultMapFromNodeModules, createVirtualTypeScriptEnvironment } from '@typescript/vfs'; +import type { VirtualTypeScriptEnvironment } from '@typescript/vfs'; import * as ts from 'typescript'; -import { ParsingError } from './parsers/parsing-error.js'; -import { ScriptParser } from './parsers/script-parser.js'; -import { isStaticMember } from './utils/attribute-utils.js'; +import type { AttributeData } from './parsers/attribute-parser.ts'; +import { ParsingError } from './parsers/parsing-error.ts'; +import { ScriptParser } from './parsers/script-parser.ts'; +import type { ValidationError } from './parsers/script-parser.ts'; +import { isStaticMember } from './utils/attribute-utils.ts'; import { createDefaultMapFromCDN, getExportedNodes, getType, inheritsFrom, isAliasedClassDeclaration -} from './utils/ts-utils.js'; +} from './utils/ts-utils.ts'; +import type { InternalNode } from './utils/ts-utils.ts'; -const toLowerCamelCase = (str) => str[0].toLowerCase() + str.substring(1); +const toLowerCamelCase = (str: string) => str[0].toLowerCase() + str.substring(1); const SUPPORTED_ATTRIBUTE_TYPES = new Set(['Curve', 'Vec2', 'Vec3', 'Vec4', 'Color', 'number', 'string', 'boolean']); +type ScriptResults = { + /** The extracted attributes, keyed by attribute name */ + attributes: Record; + /** The parsing errors for this script */ + errors: ParsingError[]; +}; + +type MemberInfo = { + /** The name of the member */ + name: string; + /** Whether the member is a script attribute */ + isAttribute: boolean; + /** The type of the member, with an array suffix when applicable */ + type: string; + /** The start of the member, including any JSDoc block */ + start: { line: number; column: number }; + /** The end of the member */ + end: { line: number; column: number }; +}; + const COMPILER_OPTIONS = { noLib: true, strict: false, @@ -31,25 +55,24 @@ const COMPILER_OPTIONS = { }; export class JSDocParser { - /** - * @type {import('@typescript/vfs').VirtualTypeScriptEnvironment} - * @private - */ - _env; + private _env: VirtualTypeScriptEnvironment; /** * An internal array of program errors - * @private */ - _errors = []; + private _errors: ValidationError[] = []; + + declare parser: ScriptParser; + + declare program: ts.Program; /** * Initializes the JSDocParser with the standard library files * - * @param {string} libPath - A path to a directory of library types, or a path to the '.d.ts' file itself - * @returns {Promise} - The initialized JSDocParser + * @param libPath - A path to a directory of library types, or a path to the '.d.ts' file itself + * @returns The initialized JSDocParser */ - async init(libPath) { + async init(libPath?: string) { if (this._env) { return this; } @@ -77,13 +100,12 @@ export class JSDocParser { /** * Creates program from fileName and scriptsContents * - * @param {[string, string][]} newFiles - An array of {[path]: [content]} which represent the files in the program - * @param {string[]} removedFiles - An array of file paths that should be removed from the program - * @returns {{ parser: ScriptParser, program: ts.Program, - * errors: import('./parsers/script-parser.js').ValidationError[]}} - The extracted attributes + * @param newFiles - An array of [path, content] tuples which represent the files in the program + * @param removedFiles - An array of file paths that should be removed from the program + * @returns The program errors */ - updateProgram(newFiles = [], removedFiles = []) { - const errors = []; + updateProgram(newFiles: [string, string][] = [], removedFiles: string[] = []) { + const errors: ValidationError[] = []; // Add custom files to the map newFiles.forEach(([filePath, fileContent]) => { @@ -112,11 +134,11 @@ export class JSDocParser { /** * Returns all the valid ESM Scripts within a file - * @param {string} fileName - The file name in the program to check - * @param {ParsingError[]} errors - An array to store any parsing errors - * @returns {Map} - A map of valid ESM Script within the file + * @param fileName - The file name in the program to check + * @param errors - An array to store any parsing errors + * @returns A map of valid ESM Script within the file */ - getAllEsmScripts(fileName, errors = []) { + getAllEsmScripts(fileName: string, errors: (ParsingError | ValidationError)[] = []) { const typeChecker = this.program.getTypeChecker(); // Find the Script class in the PlayCanvas namespace @@ -126,7 +148,7 @@ export class JSDocParser { throw new Error('PlayCanvas Types must be supplied'); } - const esmScripts = new Map(); + const esmScripts = new Map(); // Parse the source file and pc types const sourceFile = this.program.getSourceFile(fileName); @@ -144,11 +166,13 @@ export class JSDocParser { // Extract all exported nodes const nodes = getExportedNodes(this.program, sourceFile); - const esmScriptClass = pcTypes.statements.find( - (node) => node.kind === ts.SyntaxKind.ClassDeclaration && node.name.text === 'Script' + const esmScriptClass = ( + pcTypes.statements.find( + (node) => ts.isClassDeclaration(node) && node.name.text === 'Script' + ) as InternalNode )?.symbol; - const isScriptNameMember = (member) => + const isScriptNameMember = (member: ts.ClassElement) => ts.isPropertyDeclaration(member) && // Is a property declaration ts.isIdentifier(member.name) && member.name.text === 'scriptName' && @@ -157,17 +181,20 @@ export class JSDocParser { // Check if the file exports a class that inherits from `Script` nodes.forEach((node, name) => { - if (isAliasedClassDeclaration(node, typeChecker) && inheritsFrom(node, typeChecker, esmScriptClass)) { + if ( + isAliasedClassDeclaration(node, typeChecker) && + inheritsFrom(node as ts.ClassDeclaration, typeChecker, esmScriptClass) + ) { // Check for a static scriptName property and use that as the name if it exists - const scriptNameMember = node.members.find(isScriptNameMember); + const scriptNameMember = (node as ts.ClassDeclaration).members.find(isScriptNameMember); // If the scriptName property exists, use that verbatim as the name let finalName; if (scriptNameMember) { - finalName = scriptNameMember.initializer.text; + finalName = ((scriptNameMember as ts.PropertyDeclaration).initializer as ts.StringLiteral).text; } else { finalName = toLowerCamelCase(name); - const insertOffset = node.members.pos; + const insertOffset = (node as ts.ClassDeclaration).members.pos; const insertPos = node.getSourceFile().getLineAndCharacterOfPosition(insertOffset); errors.push( @@ -200,12 +227,12 @@ export class JSDocParser { /** * Analyzes the specified TypeScript file and extracts metadata from JSDoc comments. * - * @param {string} fileName - The name of the file in the program to run - * @returns {[results: object, errors: import('./parsers/script-parser.js').ValidationError[]]} - The extracted attributes + * @param fileName - The name of the file in the program to run + * @returns The extracted attributes and errors */ - parseAttributes(fileName) { - const results = {}; - const errors = [...this._errors]; + parseAttributes(fileName: string): [Record, (ParsingError | ValidationError)[]] { + const results: Record = {}; + const errors: (ParsingError | ValidationError)[] = [...this._errors]; if (errors.length > 0) { return [results, errors]; @@ -216,7 +243,7 @@ export class JSDocParser { // Extract attributes from each script nodes.forEach((node, name) => { - const opts = (results[name] = { attributes: {}, errors: [] }); + const opts = (results[name] = { attributes: {}, errors: [] as ParsingError[] }); this.parser.extractAttributes(node, { ...opts, scriptName: name }); }); @@ -225,12 +252,12 @@ export class JSDocParser { /** * Analyzes the specified TypeScript file and extracts metadata from JSDoc comments. - * @param {string} fileName - The name of the file to run - * @returns {[results: object, errors: import('./parsers/script-parser.js').ValidationError[]]} - The extracted attributes + * @param fileName - The name of the file to run + * @returns The extracted attributes and errors */ - getAttributes(fileName) { - const results = {}; - const errors = [...this._errors]; + getAttributes(fileName: string): [Record, (ParsingError | ValidationError)[]] { + const results: Record = {}; + const errors: (ParsingError | ValidationError)[] = [...this._errors]; if (errors.length > 0) { return [results, errors]; @@ -243,7 +270,7 @@ export class JSDocParser { const scripts = []; for (const [scriptName, node] of esmScripts) { const members = []; - for (const member of node.members) { + for (const member of (node as ts.ClassDeclaration).members) { if (member.kind === ts.SyntaxKind.PropertyDeclaration && !isStaticMember(member)) { members.push(member); } @@ -253,7 +280,7 @@ export class JSDocParser { } for (const { scriptName, members } of scripts) { - const localMembers = []; + const localMembers: MemberInfo[] = []; for (const member of members) { const name = member.name.getText(); const attribute = attributes[scriptName].attributes[name]; @@ -267,7 +294,8 @@ export class JSDocParser { const namePos = ts.getLineAndCharacterOfPosition(member.getSourceFile(), member.name.getStart()); - const jsdocNode = member.jsDoc && member.jsDoc[member.jsDoc.length - 1]; + const jsDocs = (member as InternalNode).jsDoc; + const jsdocNode = jsDocs && jsDocs[jsDocs.length - 1]; const jsdocPos = jsdocNode ? ts.getLineAndCharacterOfPosition(member.getSourceFile(), jsdocNode.getStart()) : null; diff --git a/src/parsers/attribute-parser.js b/src/parsers/attribute-parser.ts similarity index 64% rename from src/parsers/attribute-parser.js rename to src/parsers/attribute-parser.ts index cf1e5d9..6eb2bd6 100644 --- a/src/parsers/attribute-parser.js +++ b/src/parsers/attribute-parser.ts @@ -1,29 +1,81 @@ +import type { VirtualTypeScriptEnvironment } from '@typescript/vfs'; import * as ts from 'typescript'; -import { ParsingError } from './parsing-error.js'; -import { hasTag } from '../utils/attribute-utils.js'; -import { parseTag, validateTag } from '../utils/tag-utils.js'; -import { getLiteralValue, getType } from '../utils/ts-utils.js'; +import { hasTag } from '../utils/attribute-utils.ts'; +import { parseTag, validateTag } from '../utils/tag-utils.ts'; +import { getLiteralValue, getType } from '../utils/ts-utils.ts'; +import type { InternalNode, LiteralValue } from '../utils/ts-utils.ts'; + +import { ParsingError } from './parsing-error.ts'; + +export type TagDefinition = { + /** The type of the tag */ + type: string; + /** A function to generate a support message for the tag */ + supportMessage?: () => string; + /** A string to fix the tag */ + fix?: string; +}; + +export type TypeSerializer = (node: ts.Node, typeChecker?: ts.TypeChecker) => LiteralValue; /** - * @typedef {Object} TagDefinition - * @property {string} type - The type of the tag - * @property {() => string} supportMessage - A function to generate a support message for the tag - * @property {string} fix - A function to fix the tag + * The internal representation of a parsed attribute. Known fields are set while parsing; + * custom block tags (e.g. @placeholder, @size) land on the index signature. */ +export type AttributeData = { + /** The resolved type, replaced with its lowercased type name on output */ + type: ts.Type | string; + /** The name of the attribute */ + name: string; + /** The type name, removed on output */ + typeName?: string; + /** Whether the attribute is an array type */ + array: boolean; + /** The serialized value, moved to default on output */ + value?: LiteralValue; + /** The enum members, removed on output when empty */ + enum?: Record[]; + description?: string; + range?: number[]; + min?: number; + max?: number; + attributes?: Record; + schema?: AttributeData[]; + resource?: string; + assetType?: string; + default?: LiteralValue; + enabledif?: string; + visibleif?: string; + [key: string]: unknown; +}; /** * A class to parse JSDoc comments and extract attribute metadata. */ export class AttributeParser { + env: VirtualTypeScriptEnvironment; + + tagTypeAnnotations: Map; + + typeSerializerMap: Map; + + program: ts.Program; + + typeChecker: ts.TypeChecker; + /** * Creates a new instance of the ScriptParser class. * - * @param {Map} tags - An set of tag definitions and their validators to add to the parser - * @param {object} env - The TypeScript environment to use - * @param {Map} typeSerializerMap - A map of custom serializers + * @param tags - An set of tag definitions and their validators to add to the parser + * @param env - The TypeScript environment to use + * @param typeSerializerMap - A map of custom serializers */ - constructor(tags = new Map(), env, typeSerializerMap) { + constructor( + tags = new Map(), + env: VirtualTypeScriptEnvironment, + typeSerializerMap: Map + ) { this.env = env; this.tagTypeAnnotations = tags; this.typeSerializerMap = typeSerializerMap; @@ -35,14 +87,20 @@ export class AttributeParser { * Parses a JSdoc comment and extracts any associated metadata if the `@attribute` * modifier is used. Returns null if this is not an attribute comment. * - * @param {ts.Node} node - The node to parse - * @param {ParsingError[]} errors - An array to store any parsing errors - * @param {boolean} requiresAttributeTag - Whether the comment must have an attribute tag - * @param {string} scriptName - The name of the script this node belongs to - * @param {string} attributeName - The name of the attribute this node belongs to - * @returns {*} - The extracted metadata or null if no metadata was found + * @param node - The node to parse + * @param errors - An array to store any parsing errors + * @param requiresAttributeTag - Whether the comment must have an attribute tag + * @param scriptName - The name of the script this node belongs to + * @param attributeName - The name of the attribute this node belongs to + * @returns The extracted metadata or undefined if no metadata was found */ - parseAttributeComment(node, errors = [], requiresAttributeTag = true, scriptName, attributeName) { + parseAttributeComment( + node: ts.Node, + errors: ParsingError[] = [], + requiresAttributeTag = true, + scriptName?: string, + attributeName?: string + ) { // Check if the comment has an attribute tag if (!requiresAttributeTag || hasTag('attribute', node)) { // Fetch the primary attribute metadata @@ -52,10 +110,10 @@ export class AttributeParser { // Extract the description from the summary section. The comment is a NodeArray // rather than a string when it contains inline tags like {@link}, so flatten it - attribute.description = ts.getTextOfJSDocComment(node.jsDoc?.[0]?.comment) ?? ''; + attribute.description = ts.getTextOfJSDocComment((node as InternalNode).jsDoc?.[0]?.comment) ?? ''; // Extract metadata from custom tags - const jsDocTags = node.jsDoc?.[0]?.tags ?? []; + const jsDocTags: readonly ts.JSDocTag[] = (node as InternalNode).jsDoc?.[0]?.tags ?? []; jsDocTags.forEach((tag) => { // Only use the first line of the comment const tagName = tag.tagName.text; @@ -72,7 +130,7 @@ export class AttributeParser { validateTag(commentText, type, this.env); attribute[tagName] = value; - } catch (error) { + } catch { // generate a fix if one is provided let startPos = tag.getStart(); let endPos = tag.getEnd(); @@ -125,17 +183,18 @@ export class AttributeParser { /** * Extract the public members and their values from a TypeScript node. * - * @param {ts.Node} node - The node to parse - * @param {ParsingError[]} errors - An array to store any parsing errors - * @param {string} [scriptName] - The name of the script this node belongs to - * @returns {*} - The extracted members + * @param node - The node to parse + * @param errors - An array to store any parsing errors + * @param scriptName - The name of the script this node belongs to + * @returns The extracted members */ - getNodeAsAttribute(node, errors = [], scriptName) { - const name = node.name && ts.isIdentifier(node.name) && node.name.text; + getNodeAsAttribute(node: ts.Node, errors: ParsingError[] = [], scriptName?: string) { + const nameNode = (node as InternalNode).name; + const name = (nameNode && ts.isIdentifier(nameNode) && nameNode.text) as string; const { type, name: typeName, array, isMixedUnion } = getType(node, this.typeChecker); const enums = this.getEnumMembers(node, errors); - let value = null; + let value: LiteralValue = null; // If this is not an enum and the type is a mixed union, ie 'a' | false | 1, // we need to raise an error as this is not a supported attribute type @@ -158,7 +217,7 @@ export class AttributeParser { const serializer = !array && this.typeSerializerMap.get(typeName); if (serializer) { try { - value = serializer(node.initializer ?? node, this.typeChecker); + value = serializer((node as InternalNode).initializer ?? node, this.typeChecker); } catch (error) { // Enrich any thrown ParsingError with context if (error && typeof error === 'object') { @@ -170,22 +229,23 @@ export class AttributeParser { } } - return { type, name, typeName, array, value, enum: enums }; + const attribute: AttributeData = { type, name, typeName, array, value, enum: enums }; + return attribute; } /** * Extracts the members of an enum from a TypeScript node. * - * @param {import('typescript').Node} node - The TypeScript node to analyze - * @param {ParsingError[]} errors - An array to store any parsing errors - * @returns {Object[]} - An array of enum members + * @param node - The TypeScript node to analyze + * @param _errors - An array to store any parsing errors + * @returns An array of enum members */ - getEnumMembers(node, errors) { - let typeNode = null; - let members = []; + getEnumMembers(node: ts.Node, _errors: ParsingError[]) { + let typeNode: InternalNode | ts.JSDocTypeExpression | null = null; + let members: Record[] = []; // Check if there's a type annotation directly on the variable declaration - if (node.type) { + if ((node as InternalNode).type) { typeNode = node; } else { // Check for JSDoc annotations @@ -197,7 +257,7 @@ export class AttributeParser { } // Also consider the elementType - const type = typeNode && (typeNode.type.elementType ?? typeNode.type); + const type = typeNode && ((typeNode.type as ts.ArrayTypeNode).elementType ?? typeNode.type); if (typeNode && ts.isTypeReferenceNode(type)) { // resolve the symbol of the type @@ -216,7 +276,7 @@ export class AttributeParser { // Check if the declaration is a TypeScript enum if (ts.isEnumDeclaration(declaration)) { members = declaration.members.map((member) => { - const name = member.name.text; + const name = (member.name as ts.Identifier).text; let value; if (member.initializer) { value = getLiteralValue(member.initializer, this.typeChecker); @@ -231,7 +291,7 @@ export class AttributeParser { if (enumTag && ts.isJSDocEnumTag(enumTag)) { // This assumes enum members are correctly annotated in JSDoc comments // Example: @enum {type} EnumName { Member1, Member2 } - members = members.concat(this.getMembers(declaration.initializer, errors)); // declaration.initializer.properties.map(member => member.name.getText()); + members = members.concat(this.getMembers((declaration as InternalNode).initializer)); // declaration.initializer.properties.map(member => member.name.getText()); } }); } @@ -244,19 +304,19 @@ export class AttributeParser { /** * Extract the public members and their values from a TypeScript node. * - * @param {ts.Node} node - The node to parse - * @returns {any[]} - The extracted members + * @param node - The node to parse + * @returns The extracted members */ - getMembers(node) { - const members = []; + getMembers(node: ts.Node) { + const members: Record[] = []; if (ts.isObjectLiteralExpression(node)) { node.properties.forEach((property) => { if (ts.isPropertyAssignment(property)) { const isValidName = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name); - const name = isValidName && property.name.text; + const name = isValidName && (property.name as ts.Identifier | ts.StringLiteral).text; const value = getLiteralValue(property.initializer, this.typeChecker); - members.push({ [name]: value }); + members.push({ [name as string]: value }); } }); } diff --git a/src/parsers/parsing-error.js b/src/parsers/parsing-error.ts similarity index 59% rename from src/parsers/parsing-error.js rename to src/parsers/parsing-error.ts index 2929305..bf7bd59 100644 --- a/src/parsers/parsing-error.js +++ b/src/parsers/parsing-error.ts @@ -1,21 +1,37 @@ -/** - * @typedef {Object} Fix - * @property {string} text - The text to insert at the fix location - * @property {string} title - The title of the fix - * @property {{ startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number }} range - The range of the fix - */ +import type { Node } from 'typescript'; + +export type Fix = { + /** The text to insert at the fix location */ + text: string; + /** The title of the fix */ + title: string; + /** The range of the fix */ + range: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number }; +}; /** * A class representing a parsing error. - * @param {import('typescript').Node} node - The AST node which caused the error - * @param {string} type - The type of the error - * @param {string} message - The description of the error - * @param {Fix} [fix] - The fix for the error - * @param {string} [scriptName] - The name of the script this error belongs to (required) - * @param {string} [attributeName] - The name of the attribute this error belongs to (optional) */ export class ParsingError { - constructor(node, type, message, fix, scriptName, attributeName) { + /** The AST node which caused the error */ + node: Node; + + /** The type of the error */ + type: string; + + /** The description of the error */ + message: string; + + /** The fix for the error */ + fix?: Fix; + + /** The name of the script this error belongs to */ + scriptName?: string; + + /** The name of the attribute this error belongs to */ + attributeName?: string; + + constructor(node: Node, type: string, message: string, fix?: Fix, scriptName?: string, attributeName?: string) { this.node = node; // AST node which caused the error this.type = type; // Type of the error this.message = message; // Description of the error diff --git a/src/parsers/script-parser.js b/src/parsers/script-parser.ts similarity index 74% rename from src/parsers/script-parser.js rename to src/parsers/script-parser.ts index 04c9b06..7549016 100644 --- a/src/parsers/script-parser.js +++ b/src/parsers/script-parser.ts @@ -1,58 +1,84 @@ +import type { VirtualTypeScriptEnvironment } from '@typescript/vfs'; import * as ts from 'typescript'; -import { AttributeParser } from './attribute-parser.js'; -import { ParsingError } from './parsing-error.js'; -import { getTag, hasTag } from '../utils/attribute-utils.js'; -import { zipArrays } from '../utils/generic-utils.js'; +import { getTag, hasTag } from '../utils/attribute-utils.ts'; +import { zipArrays } from '../utils/generic-utils.ts'; import { flatMapAnyNodes, getJSDocTags, getLiteralValue, parseArrayLiteral, parseFloatNode -} from '../utils/ts-utils.js'; - -/** - * @typedef {object} Attribute - * @property {string} type - The type of the attribute - * @property {boolean} array - Whether the attribute is an array type of not - * @property {string} [description] - The description of the attribute - * @property {string} [placeholder] - Placeholder for Editor's for field UI. - * @property {*} [defaultValue] - The default value of the attribute - * @property {Object[]} [enum] - An array of possible values for the attribute - * @property {{ min: number, max: number}} [range] - Numerical constraints for the attribute - * @property {number} [precision] - Numerical constraints for the attribute - * @property {number} [step] - Numerical constraints for the attribute - * @property {string} [resource] - Name of asset type to be used in 'asset' type attribute - */ +} from '../utils/ts-utils.ts'; +import type { InternalNode } from '../utils/ts-utils.ts'; + +import { AttributeParser } from './attribute-parser.ts'; +import type { AttributeData, TagDefinition, TypeSerializer } from './attribute-parser.ts'; +import { ParsingError } from './parsing-error.ts'; +import type { Fix } from './parsing-error.ts'; + +export type Attribute = { + /** The type of the attribute */ + type: string; + /** Whether the attribute is an array type of not */ + array: boolean; + /** The description of the attribute */ + description?: string; + /** Placeholder for Editor's for field UI. */ + placeholder?: string; + /** The default value of the attribute */ + defaultValue?: unknown; + /** An array of possible values for the attribute */ + enum?: Record[]; + /** Numerical constraints for the attribute */ + range?: { min: number; max: number }; + /** Numerical constraints for the attribute */ + precision?: number; + /** Numerical constraints for the attribute */ + step?: number; + /** Name of asset type to be used in 'asset' type attribute */ + resource?: string; +}; -/** - * @typedef {object} ValidationError - * @property {string} message - The error message - * @property {string} [file] - The file the error occurred in - * @property {number} [line] - The line the error occurred on - * @property {number} [column] - The column the error occurred on - * @property {number} [length] - The length of the error - */ +export type ValidationError = { + /** The error message */ + message: string; + /** The file the error occurred in */ + file?: string; + /** The line the error occurred on */ + line?: number; + /** The column the error occurred on */ + column?: number; + /** The length of the error */ + length?: number; +}; /** * Creates a parser for new expressions. * - * @param {string} name - The name of the type - * @param {Function} argProcessor - Function to process arguments - * @param {number[]} [defaultArr] - The default array to use if the node is not a valid array - * @returns {Function} - The parser function that extracts the values from the node + * @param name - The name of the type + * @param argProcessor - Function to process arguments + * @param defaultArr - The default array to use if the node is not a valid array + * @returns The parser function that extracts the values from the node */ -const createNewExpressionParser = (name, argProcessor, defaultArr = []) => { - return (node) => { +const createNewExpressionParser = ( + name: string, + argProcessor: (args: readonly ts.Node[]) => number[], + defaultArr: number[] = [] +) => { + return (node: ts.Node) => { if (node.kind !== ts.SyntaxKind.NewExpression) { if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent)) { // scriptName/attributeName will be filled by caller if caught - throw new ParsingError(node, 'Invalid Property', `Property "${node.name.text}" is not a ${name}.`); + throw new ParsingError( + node, + 'Invalid Property', + `Property "${((node as InternalNode).name as ts.Identifier).text}" is not a ${name}.` + ); } return defaultArr; } - const args = argProcessor(node.arguments); + const args = argProcessor((node as ts.NewExpression).arguments); // combine the two arrays together, filling any missing values with the defaultArr return zipArrays(args, defaultArr); @@ -61,23 +87,23 @@ const createNewExpressionParser = (name, argProcessor, defaultArr = []) => { /** * Parses single arguments directly. - * @param {ts.Node[]} args - The arguments to parse - * @returns {number[]} - The parsed arguments + * @param args - The arguments to parse + * @returns The parsed arguments */ -const parseNumberArguments = (args) => args.map((arg) => parseFloatNode(arg, true)); +const parseNumberArguments = (args: readonly ts.Node[]) => args.map((arg) => parseFloatNode(arg, true)); // Creating specific parser instances -const createNumberArgumentParser = (name, defaultArr = []) => +const createNumberArgumentParser = (name: string, defaultArr: number[] = []) => createNewExpressionParser(name, parseNumberArguments, defaultArr); -const createArrayArgumentParser = (name, defaultArr = []) => +const createArrayArgumentParser = (name: string, defaultArr: number[] = []) => createNewExpressionParser(name, parseArrayLiteral, defaultArr); /** * Map of pc types that can be initialized with values and their corresponding argument parsers. * eg `new Vec3(1, 2, 3)` */ -const SUPPORTED_INITIALIZABLE_TYPE_NAMES = new Map([ +const SUPPORTED_INITIALIZABLE_TYPE_NAMES = new Map([ ['Curve', createArrayArgumentParser('Curve')], ['Vec2', createNumberArgumentParser('Vec2', [0, 0])], ['Vec3', createNumberArgumentParser('Vec3', [0, 0, 0])], @@ -95,10 +121,10 @@ const SUPPORTED_PLAYCANVAS_TYPES = new Set(['Curve', 'Asset', 'Entity', 'Vec2', /** * Checks if a node is a supported PlayCanvas type. - * @param {ts.Node} node - The node to check - * @returns {boolean} - True if the node is a supported PlayCanvas type + * @param node - The node to check + * @returns True if the node is a supported PlayCanvas type */ -const isSupportedPlayCanvasNode = (node) => { +const isSupportedPlayCanvasNode = (node: ts.Node) => { return ts.isClassDeclaration(node) && SUPPORTED_PLAYCANVAS_TYPES.has(node.name.text); }; @@ -135,9 +161,8 @@ const resourceTypesAsUnion = EDITOR_ASSET_TYPES.map((type) => `"${type}"`).join( /** * The set of supported block tags and their valid types and support messages when they are malformed or invalid. - * @type {Map} */ -const SUPPORTED_BLOCK_TAGS = new Map([ +const SUPPORTED_BLOCK_TAGS = new Map([ [ 'resource', { @@ -178,7 +203,7 @@ const SUPPORTED_BLOCK_TAGS = new Map([ { type: 'string', supportMessage: () => - '@placeholder is malformed or invalid. The tag should be formatted as "@placeholder \"My Placeholder\"".', + '@placeholder is malformed or invalid. The tag should be formatted as "@placeholder "My Placeholder"".', fix: '"My Placeholder"' } ], @@ -212,7 +237,7 @@ const SUPPORTED_BLOCK_TAGS = new Map([ { type: 'string', supportMessage: () => - '@title is malformed or invalid. The tag should be formatted as "@title \"My Attribute\"".', + '@title is malformed or invalid. The tag should be formatted as "@title "My Attribute"".', fix: '"My Attribute"' } ], @@ -223,10 +248,10 @@ const SUPPORTED_BLOCK_TAGS = new Map([ /** * A mapping function that transforms the Script Parsers internal representation of an attribute to a required output. - * @param {*} attribute - The attribute to map - * @returns {*} - The mapped attribute + * @param attribute - The attribute to map + * @returns The mapped attribute */ -const mapAttributesToOutput = (attribute) => { +const mapAttributesToOutput = (attribute: AttributeData) => { attribute.type = attribute.typeName.toLowerCase(); // Flatten the range into min and max @@ -295,16 +320,43 @@ const mapAttributesToOutput = (attribute) => { return attribute; }; +export type ParserOptions = { + /** The attributes object to add to */ + attributes?: Record; + /** The array to store parsing errors */ + errors?: ParsingError[]; + /** Whether the attribute requires an @attribute tag */ + requiresAttributeTag?: boolean; + /** The current recursion depth */ + depth?: number; + /** The current script name for context */ + scriptName?: string; +}; + /** * A class to parse ESM Scripts and extract attribute metadata from JSDoc comments. * This class uses the TypeScript compiler API to parse the script and extract the metadata. */ export class ScriptParser { + env: VirtualTypeScriptEnvironment; + + program: ts.Program; + + typeChecker: ts.TypeChecker; + + maxDepth: number; + + typeSerializerMap: Map; + + allSupportedTypesSet: Set; + + attributeParser: AttributeParser; + /** - * @param {import('@typescript/vfs').VirtualTypeScriptEnvironment} env - The TypeScript virtual environment - * @param {number} [maxDepth] - The maximum recursion depth + * @param env - The TypeScript virtual environment + * @param maxDepth - The maximum recursion depth */ - constructor(env, maxDepth = 2) { + constructor(env: VirtualTypeScriptEnvironment, maxDepth = 2) { if (!env) { throw new Error('A TypeScript Environment must be provided to the ScriptParser.'); } @@ -347,10 +399,10 @@ export class ScriptParser { /** * Checks a program for compile errors and returns a list of formatted error messages. - * @param {ValidationError[]} errors - The array to store error messages - * @returns {boolean} - True if the program has no errors, false otherwise + * @param errors - The array to store error messages + * @returns True if the program has no errors, false otherwise */ - validateProgram(errors = []) { + validateProgram(errors: ValidationError[] = []) { // Report any compiler errors const compilerDiagnostics = this.program.getSyntacticDiagnostics(); @@ -377,23 +429,14 @@ export class ScriptParser { return errors.length === 0; } - /** - * @typedef {Object} ParserOptions - * @property {object} [attributes] - The attributes object to add to - * @property {ParsingError[]} [errors] - The array to store parsing errors - * @property {boolean} [requiresAttributeTag] - Whether the attribute requires an @attribute tag - * @property {number} [depth] - The current recursion depth - * @property {string} [scriptName] - The current script name for context - */ - /** * Extracts Script Attributes from JSDoc style tag from a AST node * - * @param {import('typescript').Node} node - The TypeScript node to analyze - * @param {ParserOptions} [opts] - The input options - * @returns {Attribute[]} - The extracted attributes; + * @param node - The TypeScript node to analyze + * @param opts - The input options + * @returns The extracted attributes */ - extractAttributes(node, opts = {}) { + extractAttributes(node: ts.Node, opts: ParserOptions = {}) { // Default params const attributes = opts.attributes || {}; const errors = opts.errors ?? []; @@ -422,14 +465,16 @@ export class ScriptParser { // If we found metadata, extract the type and add it to the found attributes if (attributeMetadata) { - const { type, typeName } = attributeMetadata; - const initializer = member?.initializer; + // type is still the resolved ts.Type here; it only becomes a string on output + const type = attributeMetadata.type as ts.Type; + const { typeName } = attributeMetadata; + const initializer = (member as InternalNode).initializer; if (typeName === 'any') { const supportedTypes = Array.from(SUPPORTED_INITIALIZABLE_TYPE_NAMES.keys()).join(', '); let errorMessage; - if (member.initializer) { + if (initializer) { errorMessage = `This attribute is initialized with an invalid type. An attribute should be one of the following: ${supportedTypes}.`; } else { errorMessage = `This attribute has an invalid @type tag. An attribute should be one of the following: ${supportedTypes}.`; @@ -438,7 +483,8 @@ export class ScriptParser { member, 'Invalid Type', errorMessage, - node.name.getText(), + // pre-existing quirk: the script name is passed in the fix slot + (node as InternalNode).name.getText() as unknown as Fix, memberName, undefined ); @@ -449,7 +495,7 @@ export class ScriptParser { // extract attributes for the Types properties const symbol = type.aliasSymbol || type.symbol; - const typeNode = symbol?.valueDeclaration || symbol.declarations?.[0]; + const typeNode = (symbol?.valueDeclaration || symbol.declarations?.[0]) as InternalNode; if (!typeNode) { // The attribute does not have a valid type @@ -465,7 +511,7 @@ export class ScriptParser { // Check if the type is a valid interface if (!isInitialized && !typeIsInterface && !typeIsJSDocTypeDef && !isJSDocTypeLiteral) { let error; - const typeTag = getTag('type', member); + const typeTag = getTag('type', member) as ts.JSDocTypeTag | null; const supportedTypes = Array.from(SUPPORTED_INITIALIZABLE_TYPE_NAMES.keys()).join(', '); if (typeTag) { @@ -475,7 +521,7 @@ export class ScriptParser { 'Invalid Type', `"${typeTag.typeExpression.getText()}" is not a valid attribute type. An attribute should be one of the following: ${supportedTypes}`, undefined, - node.name.getText(), + (node as InternalNode).name.getText(), memberName ); } else if (initializer) { @@ -485,7 +531,7 @@ export class ScriptParser { 'Invalid Type', `"${initializer.getText()}" is not a valid attribute type. An attribute should be one of the following: ${supportedTypes}`, undefined, - node.name.getText(), + (node as InternalNode).name.getText(), memberName ); } else { @@ -495,7 +541,7 @@ export class ScriptParser { 'Invalid Type', `Attribute is an invalid type. An attribute should be one of the following: ${supportedTypes}`, undefined, - node.name.getText(), + (node as InternalNode).name.getText(), memberName ); } @@ -505,10 +551,12 @@ export class ScriptParser { } // Extract the attributes from the nested type - const members = + const members: readonly ts.Node[] = typeNode.members ?? // interface members typeNode.properties ?? // an object literals properties - Array.from(type.symbol.members).map((arr) => arr[1].declarations[0]) ?? // a typedefs property declarations + Array.from(type.symbol.members as unknown as Iterable<[string, ts.Symbol]>).map( + (entry) => entry[1].declarations[0] + ) ?? // a typedefs property declarations []; // If the type is an interface or an initialized object, we need to parse the attribute comments, if not, just the intitialized object @@ -523,11 +571,11 @@ export class ScriptParser { } // Iterate through the nested attributes and extract their metadata - const nestedAttributes = members.reduce((attributes, prop) => { + const nestedAttributes = members.reduce((attributes: Record, prop) => { const attribute = this.attributeParser.getNodeAsAttribute(prop, errors, scriptName); // If the attribute is a supported type, add it to the list of found attributes - if (attribute && this.allSupportedTypesSet.has(attribute.type)) { + if (attribute && this.allSupportedTypesSet.has(attribute.type as ts.Type)) { attributes[attribute.name] = mapAttributesToOutput(attribute); } diff --git a/src/utils/attribute-utils.js b/src/utils/attribute-utils.js deleted file mode 100644 index ef59421..0000000 --- a/src/utils/attribute-utils.js +++ /dev/null @@ -1,48 +0,0 @@ -import { getCombinedModifierFlags, ModifierFlags } from 'typescript'; - -/** - * @file Utility functions for parsing Script attributes. - */ - -/** - * Returns a JSDoc tag from a node if it exists - * @param {string} tag - The tag to search for - * @param {import("typescript").Node} node - The node to analyze - * @returns {import("typescript").JSDocTag | null} - The tag node or null if not found - */ -export const getTag = (tag, node) => { - for (const jsDoc of node?.jsDoc || []) { - for (const tagNode of jsDoc.tags || []) { - if (tagNode.tagName.text === tag) { - return tagNode; - } - } - } - return null; -}; - -/** - * Checks if a node has a specific JSDoc tag. - * @param {string} tag - The tag to search for - * @param {import("typescript").Node} node - The node to analyze - * @returns {boolean} - True if the tag is found - */ -export const hasTag = (tag, node) => { - return !!getTag(tag, node); -}; - -/** - * Checks if the JSDoc node node has an '@interface' tag - * @param {import('typescript').Node} node - The node to analyze - * @returns {boolean} - True if the node is an interface - */ -export const isInterface = (node) => { - return hasTag('interface', node); -}; - -/** - * Checks if a node is a scriptName property - * @param {import('typescript').Node} member - The node to analyze - * @returns {boolean} - True if the node is a scriptName property - */ -export const isStaticMember = (member) => (getCombinedModifierFlags(member) & ModifierFlags.Static) !== 0; diff --git a/src/utils/attribute-utils.ts b/src/utils/attribute-utils.ts new file mode 100644 index 0000000..db82de9 --- /dev/null +++ b/src/utils/attribute-utils.ts @@ -0,0 +1,51 @@ +import type { Declaration, Node } from 'typescript'; +import { getCombinedModifierFlags, ModifierFlags } from 'typescript'; + +import type { InternalNode } from './ts-utils.ts'; + +/** + * @file Utility functions for parsing Script attributes. + */ + +/** + * Returns a JSDoc tag from a node if it exists + * @param tag - The tag to search for + * @param node - The node to analyze + * @returns The tag node or null if not found + */ +export const getTag = (tag: string, node: Node) => { + for (const jsDoc of (node as InternalNode).jsDoc || []) { + for (const tagNode of jsDoc.tags || []) { + if (tagNode.tagName.text === tag) { + return tagNode; + } + } + } + return null; +}; + +/** + * Checks if a node has a specific JSDoc tag. + * @param tag - The tag to search for + * @param node - The node to analyze + * @returns True if the tag is found + */ +export const hasTag = (tag: string, node: Node) => { + return !!getTag(tag, node); +}; + +/** + * Checks if the JSDoc node node has an '@interface' tag + * @param node - The node to analyze + * @returns True if the node is an interface + */ +export const isInterface = (node: Node) => { + return hasTag('interface', node); +}; + +/** + * Checks if a node is a scriptName property + * @param member - The node to analyze + * @returns True if the node is a scriptName property + */ +export const isStaticMember = (member: Declaration) => (getCombinedModifierFlags(member) & ModifierFlags.Static) !== 0; diff --git a/src/utils/generic-utils.js b/src/utils/generic-utils.ts similarity index 61% rename from src/utils/generic-utils.js rename to src/utils/generic-utils.ts index bd6a9b4..9273c60 100644 --- a/src/utils/generic-utils.js +++ b/src/utils/generic-utils.ts @@ -2,13 +2,13 @@ * This merges two arrays together, filling in the missing values * in the first array with values from the second array. * - * @param {array} a - The first array - * @param {array} b - The second array - * @returns {array} - The merged array + * @param a - The first array + * @param b - The second array + * @returns The merged array */ -export function zipArrays(a, b) { +export function zipArrays(a: T[], b: T[]) { const length = Math.max(a.length, b.length); - const result = new Array(length); + const result = new Array(length); for (let i = 0; i < length; i++) { result[i] = i < a.length ? a[i] : b[i]; diff --git a/src/utils/tag-utils.js b/src/utils/tag-utils.ts similarity index 75% rename from src/utils/tag-utils.js rename to src/utils/tag-utils.ts index 3b73244..61e957f 100644 --- a/src/utils/tag-utils.js +++ b/src/utils/tag-utils.ts @@ -1,11 +1,12 @@ +import type { VirtualTypeScriptEnvironment } from '@typescript/vfs'; import { DiagnosticCategory } from 'typescript'; -import { parseNumber, parseStringToNumericalArray } from './ts-utils.js'; +import { parseNumber, parseStringToNumericalArray } from './ts-utils.ts'; /** * Parses a tag value into a boolean, number, array or string - * @param {string} input - The tag value to parse - * @returns {boolean|number|string|Array} - The parsed value + * @param input - The tag value to parse + * @returns The parsed value */ export function parseTag(input = '') { // Remove any leading or trailing whitespace, replace single quotes with double quotes @@ -23,12 +24,16 @@ export function parseTag(input = '') { try { const arr = parseStringToNumericalArray(input); if (Array.isArray(arr)) return arr; - } catch {} + } catch { + // not a numerical array, keep checking + } try { const arr = JSON.parse(input); if (Array.isArray(arr)) return arr; - } catch {} + } catch { + // not valid JSON, fall through to string + } // If none of the above, return the string return input; @@ -37,13 +42,13 @@ export function parseTag(input = '') { /** * Validates that a tag value matches the expected type * - * @param {String} value - The value to be validated. - * @param {String} typeAnnotation - The TypeScript type annotation as a string. - * @param {import('@typescript/vfs').VirtualTypeScriptEnvironment} env - The environment containing the language service and file creation utilities. + * @param value - The value to be validated. + * @param typeAnnotation - The TypeScript type annotation as a string. + * @param env - The environment containing the language service and file creation utilities. * @throws Will throw an error if the value does not conform to the typeAnnotation. - * @returns {true} if validation passes without type errors. + * @returns true if validation passes without type errors. */ -export function validateTag(value, typeAnnotation, env) { +export function validateTag(value: string, typeAnnotation: string, env: VirtualTypeScriptEnvironment) { const virtualFileName = '/___virtual__.ts'; const sourceText = `let a: ${typeAnnotation} = ${value};`; diff --git a/src/utils/ts-utils.js b/src/utils/ts-utils.ts similarity index 68% rename from src/utils/ts-utils.js rename to src/utils/ts-utils.ts index 9a38876..bb58f4a 100644 --- a/src/utils/ts-utils.js +++ b/src/utils/ts-utils.ts @@ -1,11 +1,30 @@ import { knownLibFilesForCompilerOptions } from '@typescript/vfs'; import * as ts from 'typescript'; -import { ParsingError } from '../parsers/parsing-error.js'; +import { ParsingError } from '../parsers/parsing-error.ts'; -export function createDefaultMapFromCDN(options, prefix, ts) { - const fsMap = new Map(); - const files = knownLibFilesForCompilerOptions(options, ts); +/** + * A TS AST node with internal properties that are absent from the public type definitions. + */ +export type InternalNode = ts.Node & { + jsDoc?: ts.JSDoc[]; + initializer?: ts.Expression; + name?: ts.DeclarationName; + type?: ts.TypeNode; + members?: ts.NodeArray; + properties?: ts.NodeArray; + symbol?: ts.Symbol; +}; + +/** + * A literal value extracted from an AST node. + */ +export type LiteralValue = + string | number | boolean | null | undefined | ts.PseudoBigInt | LiteralValue[] | { [key: string]: LiteralValue }; + +export function createDefaultMapFromCDN(options: ts.CompilerOptions, prefix: string, tsLib: typeof ts) { + const fsMap = new Map(); + const files = knownLibFilesForCompilerOptions(options, tsLib); async function uncached() { const contentPromises = files.map((lib) => { @@ -29,11 +48,11 @@ export function createDefaultMapFromCDN(options, prefix, ts) { /** * Resolves the original symbol in case of alias. - * @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker - * @param {import('typescript').Symbol} symbol - The TypeScript symbol - * @returns {import('typescript').Symbol} - The resolved symbol + * @param typeChecker - The TypeScript type checker + * @param symbol - The TypeScript symbol + * @returns The resolved symbol */ -function resolveAliasedSymbol(typeChecker, symbol) { +function resolveAliasedSymbol(typeChecker: ts.TypeChecker, symbol: ts.Symbol) { while (symbol.flags & ts.SymbolFlags.Alias) { symbol = typeChecker.getAliasedSymbol(symbol); } @@ -41,33 +60,33 @@ function resolveAliasedSymbol(typeChecker, symbol) { } /** - * Returns an array of exported nodes from a TypeScript source file. - * @param {import('typescript').Program} program - The TypeScript program - * @param {import('typescript').SourceFile} sourceFile - The TypeScript source file - * @returns {let map: Map>} - A Map of exported nodes + * Returns a map of exported nodes from a TypeScript source file. + * @param program - The TypeScript program + * @param sourceFile - The TypeScript source file + * @returns A Map of exported nodes */ -export function getExportedNodes(program, sourceFile) { +export function getExportedNodes(program: ts.Program, sourceFile: ts.SourceFile) { + // Find the actual declaration nodes for each exported symbol + const exportedNodes = new Map(); + if (!program || !sourceFile) { - return []; + return exportedNodes; } const typeChecker = program.getTypeChecker(); const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile); if (!moduleSymbol) { - return []; + return exportedNodes; } const exportedSymbols = typeChecker.getExportsOfModule(moduleSymbol); - // Find the actual declaration nodes for each exported symbol - const exportedNodes = new Map(); - exportedSymbols.forEach((symbol) => { const resolvedSymbol = resolveAliasedSymbol(typeChecker, symbol); if (resolvedSymbol.declarations) { const node = resolvedSymbol.declarations[0]; - const name = symbol.name === 'default' ? node.name.getText() : symbol.name; + const name = symbol.name === 'default' ? (node as InternalNode).name.getText() : symbol.name; exportedNodes.set(name, node); } }); @@ -78,17 +97,18 @@ export function getExportedNodes(program, sourceFile) { /** * Checks if the node or its aliased symbol is a class declaration. * - * @param {import('typescript').Node} node - The TypeScript node - * @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker - * @returns {boolean} - True if the node or its original declaration is a class declaration, false otherwise + * @param node - The TypeScript node + * @param typeChecker - The TypeScript type checker + * @returns True if the node or its original declaration is a class declaration, false otherwise */ -export function isAliasedClassDeclaration(node, typeChecker) { +export function isAliasedClassDeclaration(node: ts.Node, typeChecker: ts.TypeChecker) { if (ts.isClassDeclaration(node)) { return true; } // Try to resolve the original declaration - if (node?.name && ts.isIdentifier(node.name)) { - const symbol = typeChecker.getSymbolAtLocation(node.name); + const name = (node as InternalNode).name; + if (name && ts.isIdentifier(name)) { + const symbol = typeChecker.getSymbolAtLocation(name); if (symbol) { const resolvedSymbol = resolveAliasedSymbol(typeChecker, symbol); if (resolvedSymbol.declarations) { @@ -101,12 +121,12 @@ export function isAliasedClassDeclaration(node, typeChecker) { /** * Determines if a TypeScript node inherits from a specific base class. - * @param {ts.ClassDeclaration} node - The TypeScript node to analyze - * @param {ts.TypeChecker} typeChecker - The TypeScript type checker - * @param {ts.Symbol} baseClassSymbol - The base class symbol to check inheritance against - * @returns {boolean} - True if the node inherits from the base class + * @param node - The TypeScript node to analyze + * @param typeChecker - The TypeScript type checker + * @param baseClassSymbol - The base class symbol to check inheritance against + * @returns True if the node inherits from the base class */ -export function inheritsFrom(node, typeChecker, baseClassSymbol) { +export function inheritsFrom(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker, baseClassSymbol: ts.Symbol) { if (node.heritageClauses) { for (const clause of node.heritageClauses) { for (const type of clause.types) { @@ -121,7 +141,7 @@ export function inheritsFrom(node, typeChecker, baseClassSymbol) { // Get the declaration of the symbol to check its heritage clauses if (symbol?.declarations.length > 0) { const declaration = symbol.declarations[0]; - if (inheritsFrom(declaration, typeChecker, baseClassSymbol)) { + if (inheritsFrom(declaration as ts.ClassDeclaration, typeChecker, baseClassSymbol)) { return true; } } @@ -133,14 +153,14 @@ export function inheritsFrom(node, typeChecker, baseClassSymbol) { /** * Recursively gathers and flat maps and nodes that meet a specific filter. - * @param {import('typescript').Node} node - The TypeScript node to analyze - * @param {function} filter - A function to filter the nodes - * @returns {import('typescript').Node[]} - An array of exported class declarations + * @param node - The TypeScript node to analyze + * @param filter - A function to filter the nodes + * @returns An array of exported class declarations */ -export function flatMapAnyNodes(node, filter = () => true) { - const results = []; +export function flatMapAnyNodes(node: ts.Node, filter: (node: ts.Node) => boolean = () => true) { + const results: ts.Node[] = []; - function visit(node) { + function visit(node: ts.Node) { // Check if this node is an exported class declaration if (filter(node)) { results.push(node); @@ -159,11 +179,11 @@ export function flatMapAnyNodes(node, filter = () => true) { /** * Takes a AST Node, and if it's a class declaration, it will return it's super classes * - * @param {import('typescript').Node} node - The node to check - * @param {import('typescript').TypeChecker} typeChecker - The programs typechecker - * @returns {import('typescript').Node[]} - The analyzed node and any super classes in the inheritance chain + * @param node - The node to check + * @param typeChecker - The programs typechecker + * @returns The analyzed node and any super classes in the inheritance chain */ -function getSuperClasses(node, typeChecker) { +function getSuperClasses(node: ts.ClassDeclaration | ts.InterfaceDeclaration, typeChecker: ts.TypeChecker) { const superClasses = [node]; let currentClass = node; @@ -201,13 +221,12 @@ function getSuperClasses(node, typeChecker) { /** * Retrieves the JSDoc-style comments associated with a specific AST node. * - * @param {import('typescript').Node} node - The TypeScript node to analyze - * @param {string} text - The full text content of the source file - * @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker - * @returns {{ memberName: string, member: Node, range: import('typescript').CommentRange}[]} - An array of comment ranges + * @param node - The TypeScript node to analyze + * @param typeChecker - The TypeScript type checker + * @returns An array of members with JSDoc comments */ -export function getJSDocTags(node, typeChecker) { - const tags = []; +export function getJSDocTags(node: ts.Node, typeChecker: ts.TypeChecker) { + const tags: { memberName: string; member: ts.Node }[] = []; // check if class interface of initialized object if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) { @@ -219,17 +238,20 @@ export function getJSDocTags(node, typeChecker) { // iterate over the heritance chain heritageChain.forEach((classNode) => { // for each class iterate over it's class members - classNode.members.forEach((member) => { - const isSetter = ts.isSetAccessor(member); - if (ts.isPropertyDeclaration(member) || isSetter || ts.isPropertySignature(member)) { - const memberName = member.name && ts.isIdentifier(member.name) ? member.name.text : 'unnamed'; - + const classMembers: readonly ts.Node[] = classNode.members; + classMembers.forEach((classMember) => { + const isSetter = ts.isSetAccessor(classMember); + if (ts.isPropertyDeclaration(classMember) || isSetter || ts.isPropertySignature(classMember)) { + const memberName = + classMember.name && ts.isIdentifier(classMember.name) ? classMember.name.text : 'unnamed'; + + let member: InternalNode = classMember; let hasJsDoc = member.jsDoc?.length > 0; // If we are a setter with no jsdoc block, check any associated getter if (isSetter && !hasJsDoc) { - const symbol = typeChecker.getSymbolAtLocation(member.name); - member = symbol.declarations.find((accessor) => accessor.jsDoc?.length > 0); + const symbol = typeChecker.getSymbolAtLocation(classMember.name); + member = symbol.declarations.find((accessor) => (accessor as InternalNode).jsDoc?.length > 0); hasJsDoc = !!member; } @@ -251,7 +273,7 @@ export function getJSDocTags(node, typeChecker) { ? property.name.text : 'unnamed'; - if (property.jsDoc?.length > 0) { + if ((property as InternalNode).jsDoc?.length > 0) { tags.push({ memberName, member: property @@ -266,10 +288,10 @@ export function getJSDocTags(node, typeChecker) { /** * Determines if a TypeScript node is an enum. - * @param {import('typescript').Node} node - The TypeScript node to analyze - * @returns {boolean} - True if the node is an enum + * @param node - The TypeScript node to analyze + * @returns True if the node is an enum */ -export function isEnum(node) { +export function isEnum(node: ts.Node) { // Check for TypeScript native enum if (ts.isEnumDeclaration(node)) { return true; // The node is a TypeScript enum @@ -288,11 +310,11 @@ export function isEnum(node) { /** * Determines the primitive type for enums, or falls back to the actual type name. * - * @param {ts.Type} type - The type to inspect. - * @param {ts.TypeChecker} typeChecker - The TypeScript type checker. - * @returns {'string' | 'boolean' | 'number' | null} - The primitive type of the enum or the type's name. + * @param type - The type to inspect. + * @param typeChecker - The TypeScript type checker. + * @returns The primitive type of the enum or null. */ -export function getPrimitiveEnumType(type, typeChecker) { +export function getPrimitiveEnumType(type: ts.Type, typeChecker: ts.TypeChecker) { // Check if the type is an enum type const isEnumType = type.symbol?.declarations?.some((decl) => ts.isEnumDeclaration(decl)); @@ -306,7 +328,7 @@ export function getPrimitiveEnumType(type, typeChecker) { // Get the enum members let enumMembers; if (isEnumType) { - enumMembers = type.symbol.declarations[0].members; + enumMembers = (type.symbol.declarations[0] as ts.EnumDeclaration).members; } else if (isEnumMember) { const enumMember = type.symbol.declarations[0]; const parentEnum = enumMember.parent; @@ -331,23 +353,26 @@ export function getPrimitiveEnumType(type, typeChecker) { /** * Gets the inferred type of a TypeScript node. * - * @param {ts.Node} node - The TypeScript node to analyze - * @param {ts.TypeChecker} typeChecker - The TypeScript type checker - * @returns {{ array: boolean, name: string | undefined, type: Type | null }} - The inferred type of the node + * @param node - The TypeScript node to analyze + * @param typeChecker - The TypeScript type checker + * @returns The inferred type of the node */ -export function getType(node, typeChecker) { - if (node?.name?.kind === ts.SyntaxKind.Identifier) { +export function getType(node: ts.Node, typeChecker: ts.TypeChecker) { + if ((node as InternalNode).name?.kind === ts.SyntaxKind.Identifier) { const type = typeChecker.getTypeAtLocation(node); const array = typeChecker.isArrayType(type); - const actualType = array ? typeChecker.getElementTypeOfArrayType(type) : type; + // getElementTypeOfArrayType is an internal TS API absent from the public types + const checker = typeChecker as ts.TypeChecker & { getElementTypeOfArrayType(type: ts.Type): ts.Type }; + const actualType = array ? checker.getElementTypeOfArrayType(type) : type; // A type could be a literal union, such as '1 | 2 | 3', so we need to get the base type const baseType = typeChecker.getBaseTypeOfLiteralType(actualType); // If baseType has multiple types, it means we have a mixed union, ie 'a' | false | 1. let isMixedUnion = false; - if (Array.isArray(baseType.types)) { - const primitiveKinds = new Set(baseType.types.map((t) => typeChecker.getBaseTypeOfLiteralType(t))); + const unionTypes = (baseType as ts.UnionType).types; + if (Array.isArray(unionTypes)) { + const primitiveKinds = new Set(unionTypes.map((t) => typeChecker.getBaseTypeOfLiteralType(t))); primitiveKinds.delete(null); isMixedUnion = primitiveKinds.size > 1; } @@ -364,10 +389,10 @@ export function getType(node, typeChecker) { * Parses a numerical string into a number. Handles hexadecimal, binary, * octal, exponential notation and underscore notation * - * @param {string} input - The string to parse - * @returns {number} The parsed number, or NaN if the input is not a number + * @param input - The string to parse + * @returns The parsed number, or NaN if the input is not a number */ -export function parseNumber(input) { +export function parseNumber(input: string) { // Remove underscores from the string to handle underscore notation let cleanedInput = input.replace(/_/g, '').toLowerCase(); @@ -411,11 +436,11 @@ export function parseNumber(input) { /** * Parses a string representation of a numerical array into an array of numbers. * - * @param {string} input - The string to parse - * @returns {number[]} - The parsed array of numbers - * @throws {Error} - If the input contains invalid characters or is not a valid array + * @param input - The string to parse + * @returns The parsed array of numbers + * @throws If the input contains invalid characters or is not a valid array */ -export function parseStringToNumericalArray(input) { +export function parseStringToNumericalArray(input: string) { // Remove unnecessary whitespace const sanitizedInput = input.replace(/\s+/g, ''); @@ -443,16 +468,16 @@ export function parseStringToNumericalArray(input) { /** * Checks if the given node is a numerical literal or a unary expression. - * @param {import('typescript').Node} node - The node to check - * @returns {boolean} - True if the node is a numeric literal + * @param node - The node to check + * @returns True if the node is a numeric literal */ -export const isNumericLiteral = (node) => { +export const isNumericLiteral = (node: ts.Node) => { // Is the node a numeric literal '10' const isNumericLiteral = ts.isNumericLiteral(node); // Or is it a +/- unary expression '-10' or '+10' const isUnaryExpression = - ts.isUnaryExpression(node) && // + ts.isPrefixUnaryExpression(node) && // (node.operator === ts.SyntaxKind.MinusToken || node.operator === ts.SyntaxKind.PlusToken) && node.operand.kind === ts.SyntaxKind.NumericLiteral; @@ -461,12 +486,12 @@ export const isNumericLiteral = (node) => { /** * If the given node is a numeric literal, returns the parsed number. - * @param {import('typescript').Node} node - The node to check - * @param {boolean} [isArgument] - True if the node is an argument in a parameter list - * @returns {number} - The parsed number - * @throws {ParsingError} - If the node is not a numeric literal + * @param node - The node to check + * @param isArgument - True if the node is an argument in a parameter list + * @returns The parsed number + * @throws {ParsingError} If the node is not a numeric literal */ -export const parseFloatNode = (node, isArgument = false) => { +export const parseFloatNode = (node: ts.Node, isArgument = false) => { if (!isNumericLiteral(node)) { if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent) || isArgument) { throw new ParsingError( @@ -482,10 +507,10 @@ export const parseFloatNode = (node, isArgument = false) => { /** * If the given node is a boolean literal, returns the parsed boolean. - * @param {import('typescript').Node} node - The node to check - * @returns {boolean} - The parsed boolean + * @param node - The node to check + * @returns The parsed boolean */ -export const parseBooleanNode = (node) => { +export const parseBooleanNode = (node: ts.Node) => { if (node.kind !== ts.SyntaxKind.TrueKeyword && node.kind !== ts.SyntaxKind.FalseKeyword) { if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent)) { throw new ParsingError( @@ -499,7 +524,7 @@ export const parseBooleanNode = (node) => { return node.kind === ts.SyntaxKind.TrueKeyword; }; -function resolveIdentifier(node, typeChecker) { +function resolveIdentifier(node: ts.Node, typeChecker: ts.TypeChecker) { const symbol = typeChecker.getSymbolAtLocation(node); if (symbol && symbol.declarations) { for (const declaration of symbol.declarations) { @@ -518,11 +543,11 @@ function resolveIdentifier(node, typeChecker) { * Resolve the value of a property access expression. Limited to simple cases like * object literals and variable declarations. * - * @param {import('typescript').Node} node - The property access expression node - * @param {import('typescript')} typeChecker - The TypeScript type checker - * @returns {any} - The resolved value of the property access + * @param node - The property access expression node + * @param typeChecker - The TypeScript type checker + * @returns The resolved value of the property access */ -const resolvePropertyAccess = (node, typeChecker) => { +const resolvePropertyAccess = (node: ts.PropertyAccessExpression, typeChecker: ts.TypeChecker) => { const symbol = typeChecker.getSymbolAtLocation(node); if (symbol && symbol.declarations) { for (const declaration of symbol.declarations) { @@ -552,7 +577,7 @@ const resolvePropertyAccess = (node, typeChecker) => { const objValue = getLiteralValue(node.expression, typeChecker); if (objValue && typeof objValue === 'object') { const propName = node.name.text; - return objValue[propName]; + return (objValue as Record)[propName]; } return undefined; @@ -560,29 +585,29 @@ const resolvePropertyAccess = (node, typeChecker) => { /** * Evaluates unary prefixes like +, -, !, ~, and returns the result. - * @param {import('typescript').Node} node - The AST node to evaluate - * @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker - * @returns {number | boolean | undefined} - The result of the evaluation + * @param node - The AST node to evaluate + * @param typeChecker - The TypeScript type checker + * @returns The result of the evaluation */ -const evaluatePrefixUnaryExpression = (node, typeChecker) => { +const evaluatePrefixUnaryExpression = (node: ts.PrefixUnaryExpression, typeChecker: ts.TypeChecker) => { const operandValue = getLiteralValue(node.operand, typeChecker); if (operandValue !== undefined) { switch (node.operator) { case ts.SyntaxKind.PlusToken: - return +operandValue; + return +(operandValue as number); case ts.SyntaxKind.MinusToken: - return -operandValue; + return -(operandValue as number); case ts.SyntaxKind.ExclamationToken: return !operandValue; case ts.SyntaxKind.TildeToken: - return ~operandValue; + return ~(operandValue as number); } } return undefined; }; -function handleObjectLiteral(node, typeChecker) { - const obj = {}; +function handleObjectLiteral(node: ts.ObjectLiteralExpression, typeChecker: ts.TypeChecker) { + const obj: Record = {}; node.properties.forEach((prop) => { if (ts.isPropertyAssignment(prop)) { const key = prop.name.getText(); @@ -602,11 +627,11 @@ function handleObjectLiteral(node, typeChecker) { * supports various types of literals and expressions, including object literals, * array literals, identifiers, and unary expressions. * - * @param {import('typescript').Node} node - The AST node to evaluate - * @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker - * @returns {any} - The extracted literal value + * @param node - The AST node to evaluate + * @param typeChecker - The TypeScript type checker + * @returns The extracted literal value */ -export function getLiteralValue(node, typeChecker) { +export function getLiteralValue(node: ts.Node, typeChecker?: ts.TypeChecker): LiteralValue { if (!node) return undefined; // Try typeChecker approach first for imported constants and other complex cases @@ -617,7 +642,8 @@ export function getLiteralValue(node, typeChecker) { } } - if (ts.isLiteralExpression(node) || ts.isBooleanLiteral(node)) { + const isBooleanLiteral = node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword; + if (ts.isLiteralExpression(node) || isBooleanLiteral) { if (ts.isStringLiteral(node)) { return node.text; } @@ -636,17 +662,17 @@ export function getLiteralValue(node, typeChecker) { case ts.SyntaxKind.NullKeyword: return null; case ts.SyntaxKind.ArrayLiteralExpression: - return node.elements.map((element) => getLiteralValue(element, typeChecker)); + return (node as ts.ArrayLiteralExpression).elements.map((element) => getLiteralValue(element, typeChecker)); case ts.SyntaxKind.ObjectLiteralExpression: - return handleObjectLiteral(node, typeChecker); + return handleObjectLiteral(node as ts.ObjectLiteralExpression, typeChecker); case ts.SyntaxKind.Identifier: return resolveIdentifier(node, typeChecker); case ts.SyntaxKind.PropertyAccessExpression: - return resolvePropertyAccess(node, typeChecker); + return resolvePropertyAccess(node as ts.PropertyAccessExpression, typeChecker); case ts.SyntaxKind.ParenthesizedExpression: - return getLiteralValue(node.expression, typeChecker); + return getLiteralValue((node as ts.ParenthesizedExpression).expression, typeChecker); case ts.SyntaxKind.PrefixUnaryExpression: - return evaluatePrefixUnaryExpression(node, typeChecker); + return evaluatePrefixUnaryExpression(node as ts.PrefixUnaryExpression, typeChecker); default: return undefined; } @@ -654,10 +680,10 @@ export function getLiteralValue(node, typeChecker) { /** * If the given node is a string literal, returns the parsed string. - * @param {import('typescript').Node} node - The node to check - * @returns {string} - The parsed string + * @param node - The node to check + * @returns The parsed string */ -export const parseStringNode = (node) => { +export const parseStringNode = (node: ts.Node) => { if (node.kind !== ts.SyntaxKind.StringLiteral) { if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent)) { throw new ParsingError( @@ -671,7 +697,7 @@ export const parseStringNode = (node) => { return node.getText().slice(1, -1); }; -export const parseArrayLiteralNode = (node, isArgument = false) => { +export const parseArrayLiteralNode = (node: ts.Node, isArgument = false) => { if (node.kind !== ts.SyntaxKind.ArrayLiteralExpression) { if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent) || isArgument) { throw new ParsingError( @@ -687,10 +713,10 @@ export const parseArrayLiteralNode = (node, isArgument = false) => { /** * Parses the first argument as an array literal. - * @param {ts.Node[]} args - The arguments to parse - * @returns {number[]} - The parsed array + * @param args - The arguments to parse + * @returns The parsed array */ -export const parseArrayLiteral = (args) => { +export const parseArrayLiteral = (args: readonly ts.Node[]) => { if (args.length > 0) { return parseArrayLiteralNode(args[0], true); } diff --git a/test/tests/invalid/asset.test.js b/test/tests/invalid/asset.test.js index 38d8e3d..f44d4f3 100644 --- a/test/tests/invalid/asset.test.js +++ b/test/tests/invalid/asset.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('INVALID: Asset attribute', function () { let data; diff --git a/test/tests/invalid/curve.test.js b/test/tests/invalid/curve.test.js index d997dd9..901c2d7 100644 --- a/test/tests/invalid/curve.test.js +++ b/test/tests/invalid/curve.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('INVALID: Curve attribute', function () { let data; diff --git a/test/tests/invalid/json.test.js b/test/tests/invalid/json.test.js index 10d6db3..4d6a97b 100644 --- a/test/tests/invalid/json.test.js +++ b/test/tests/invalid/json.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('INVALID: JSON attribute', function () { let data; diff --git a/test/tests/invalid/literal-unions.test.js b/test/tests/invalid/literal-unions.test.js index c842fd5..faa768e 100644 --- a/test/tests/invalid/literal-unions.test.js +++ b/test/tests/invalid/literal-unions.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('INVALID: Mixed literal union types', function () { let data; diff --git a/test/tests/invalid/misc.test.js b/test/tests/invalid/misc.test.js index 2539e9d..0e21287 100644 --- a/test/tests/invalid/misc.test.js +++ b/test/tests/invalid/misc.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('INVALID: Misc attribute types', function () { let data; diff --git a/test/tests/invalid/numeric.test.js b/test/tests/invalid/numeric.test.js index caf9783..2b7fbd1 100644 --- a/test/tests/invalid/numeric.test.js +++ b/test/tests/invalid/numeric.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('INVALID: Numeric attribute', function () { let data; diff --git a/test/tests/invalid/program.test.js b/test/tests/invalid/program.test.js index b4e3049..f2105ac 100644 --- a/test/tests/invalid/program.test.js +++ b/test/tests/invalid/program.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('INVALID: Program ', function () { let data; diff --git a/test/tests/invalid/vector.test.js b/test/tests/invalid/vector.test.js index c246d2a..6302fdf 100644 --- a/test/tests/invalid/vector.test.js +++ b/test/tests/invalid/vector.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('INVALID: Vector attribute', function () { let data; diff --git a/test/tests/valid/asset.test.js b/test/tests/valid/asset.test.js index 36c6cc9..27eeb35 100644 --- a/test/tests/valid/asset.test.js +++ b/test/tests/valid/asset.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; function runTests(fileName) { const isTS = fileName.endsWith('.ts'); diff --git a/test/tests/valid/checkbox.test.js b/test/tests/valid/checkbox.test.js index 9fc3f93..c1c820d 100644 --- a/test/tests/valid/checkbox.test.js +++ b/test/tests/valid/checkbox.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; function runTests(fileName) { const isTS = fileName.endsWith('.ts'); diff --git a/test/tests/valid/color.test.js b/test/tests/valid/color.test.js index f72dcfa..8043ec9 100644 --- a/test/tests/valid/color.test.js +++ b/test/tests/valid/color.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Color attribute', function () { let data; diff --git a/test/tests/valid/conditional.test.js b/test/tests/valid/conditional.test.js index 328b883..9bc963c 100644 --- a/test/tests/valid/conditional.test.js +++ b/test/tests/valid/conditional.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Conditional Tags', function () { let data; diff --git a/test/tests/valid/curve.test.js b/test/tests/valid/curve.test.js index 7c9c882..469f4f7 100644 --- a/test/tests/valid/curve.test.js +++ b/test/tests/valid/curve.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Curve attribute', function () { let data; diff --git a/test/tests/valid/empty.test.js b/test/tests/valid/empty.test.js index b91d19a..e328a7b 100644 --- a/test/tests/valid/empty.test.js +++ b/test/tests/valid/empty.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Program ', function () { let data; diff --git a/test/tests/valid/entity.test.js b/test/tests/valid/entity.test.js index 12c97f5..41e0988 100644 --- a/test/tests/valid/entity.test.js +++ b/test/tests/valid/entity.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Entity attribute', function () { let data; diff --git a/test/tests/valid/enum.test.js b/test/tests/valid/enum.test.js index d3f9d9f..ba5b502 100644 --- a/test/tests/valid/enum.test.js +++ b/test/tests/valid/enum.test.js @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; import { TONEMAP_LINEAR, TONEMAP_FILMIC, TONEMAP_HEJL, TONEMAP_ACES, TONEMAP_ACES2, TONEMAP_NEUTRAL } from 'playcanvas'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; function runTests(fileName) { const isTS = fileName.endsWith('.ts'); diff --git a/test/tests/valid/export.test.js b/test/tests/valid/export.test.js index 84ee599..9d12b0d 100644 --- a/test/tests/valid/export.test.js +++ b/test/tests/valid/export.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Export Script', function () { let data; diff --git a/test/tests/valid/inherit.test.js b/test/tests/valid/inherit.test.js index 4835250..41d73ac 100644 --- a/test/tests/valid/inherit.test.js +++ b/test/tests/valid/inherit.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Script inheritance attributes', function () { let data; diff --git a/test/tests/valid/intellisense.test.js b/test/tests/valid/intellisense.test.js index c618b81..a810150 100644 --- a/test/tests/valid/intellisense.test.js +++ b/test/tests/valid/intellisense.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { getAttributes } from '../../utils.js'; +import { getAttributes } from '../../utils.ts'; describe('VALID: Intellisense Parsing', function () { let data; diff --git a/test/tests/valid/json.test.js b/test/tests/valid/json.test.js index e84e655..e022823 100644 --- a/test/tests/valid/json.test.js +++ b/test/tests/valid/json.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('JS: VALID: Asset attribute', function () { let data; diff --git a/test/tests/valid/literal-unions.test.js b/test/tests/valid/literal-unions.test.js index b652f1e..6e6e604 100644 --- a/test/tests/valid/literal-unions.test.js +++ b/test/tests/valid/literal-unions.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Literal union types', function () { let data; diff --git a/test/tests/valid/misc.test.js b/test/tests/valid/misc.test.js index 71e8710..4c08c85 100644 --- a/test/tests/valid/misc.test.js +++ b/test/tests/valid/misc.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Misc attribute types', function () { let data; diff --git a/test/tests/valid/numeric.test.js b/test/tests/valid/numeric.test.js index bbada36..f80d191 100644 --- a/test/tests/valid/numeric.test.js +++ b/test/tests/valid/numeric.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Numeric attribute', function () { let data; diff --git a/test/tests/valid/program.test.js b/test/tests/valid/program.test.js index 6867bec..a61fb5d 100644 --- a/test/tests/valid/program.test.js +++ b/test/tests/valid/program.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Program ', function () { let data; diff --git a/test/tests/valid/text.test.js b/test/tests/valid/text.test.js index aec9543..e8f3787 100644 --- a/test/tests/valid/text.test.js +++ b/test/tests/valid/text.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Text attribute', function () { let data; diff --git a/test/tests/valid/vector.test.js b/test/tests/valid/vector.test.js index 189cfe7..03a0b9c 100644 --- a/test/tests/valid/vector.test.js +++ b/test/tests/valid/vector.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; -import { parseAttributes } from '../../utils.js'; +import { parseAttributes } from '../../utils.ts'; describe('VALID: Vector attribute', function () { let data; diff --git a/test/utils.js b/test/utils.ts similarity index 61% rename from test/utils.js rename to test/utils.ts index ba5ad9a..1212aab 100644 --- a/test/utils.js +++ b/test/utils.ts @@ -2,17 +2,17 @@ import fs from 'fs/promises'; import path, { dirname } from 'path'; import { fileURLToPath } from 'url'; -import { JSDocParser } from '../src/index.js'; +import { JSDocParser } from '../src/index.ts'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** * Reads the contents of a file and returns an array containing the file name and its contents. - * @param {string} filePath - The path to the file. - * @returns {Promise<[string, string]>} A promise that resolves to an array containing the file name and its contents. + * @param filePath - The path to the file. + * @returns A promise that resolves to an array containing the file name and its contents. */ -async function getFileData(filePath) { +async function getFileData(filePath: string): Promise<[string, string]> { try { const contents = await fs.readFile(path.join(__dirname, 'fixtures', filePath), 'utf8'); return [path.join('/', filePath), contents]; @@ -24,9 +24,9 @@ async function getFileData(filePath) { /** * Loads the playcanvas types file from node_modules. - * @returns {Promise<[string, string]>} A promise that resolves to an array containing the file path and its contents. + * @returns A promise that resolves to an array containing the file path and its contents. */ -async function getPlaycanvasTypes() { +async function getPlaycanvasTypes(): Promise<[string, string]> { const typesUrl = import.meta.resolve('playcanvas/build/playcanvas.d.ts'); const typesPath = fileURLToPath(typesUrl); const contents = await fs.readFile(typesPath, 'utf8'); @@ -36,20 +36,19 @@ async function getPlaycanvasTypes() { /** * Fetches the contents of multiple files. - * @param {string[]} filePaths - An array of file paths. - * @returns {Promise<[string, string][]>} A promise that resolves to an array containing the file names and their contents. + * @param filePaths - An array of file paths. + * @returns A promise that resolves to an array containing the file names and their contents. */ -export function fetchScripts(filePaths) { +export function fetchScripts(filePaths: string[]) { return Promise.all(filePaths.map(getFileData)); } /** - * @param {string[]} paths - The path to the example file. - * @returns {Promise<[results: any, - * errors: import('../src/parsers/script-parser.js').ValidationError[]]>} A promise that resolves to - * an array of attribute data. + * Parses the attributes of the given example files. + * @param paths - The paths to the example files. + * @returns A promise that resolves to the extracted attributes and errors. */ -export async function parseAttributes(...paths) { +export async function parseAttributes(...paths: string[]) { const parser = new JSDocParser(); await parser.init(); const scriptContents = await fetchScripts(paths); @@ -58,7 +57,12 @@ export async function parseAttributes(...paths) { return parser.parseAttributes(paths[0]); } -export async function getAttributes(...paths) { +/** + * Gets the attribute members of the given example files. + * @param paths - The paths to the example files. + * @returns A promise that resolves to the extracted members and errors. + */ +export async function getAttributes(...paths: string[]) { const parser = new JSDocParser(); await parser.init(); const scriptContents = await fetchScripts(paths); diff --git a/tsconfig.build.json b/tsconfig.build.json index b7ee816..956a3bb 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,8 +1,10 @@ { - "files": ["src/index.js"], + "extends": "./tsconfig.json", + "files": ["src/index.ts"], + "include": [], "compilerOptions": { - "allowJs": true, - "lib": ["es2019", "dom", "dom.iterable"], + "noEmit": false, + "sourceMap": false, "declaration": true, "emitDeclarationOnly": true, "outDir": "types" diff --git a/tsconfig.json b/tsconfig.json index 8721f1d..d9408f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,12 @@ { "compilerOptions": { + "allowImportingTsExtensions": true, "allowJs": true, + "erasableSyntaxOnly": true, "esModuleInterop": true, + "noEmit": true, "lib": ["es2019", "dom", "dom.iterable"], + "module": "esnext", "moduleResolution": "bundler", "noImplicitAny": true, "outDir": "dist",