diff --git a/tsconfig.json b/tsconfig.json index 5c2803f..7421ad2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -60,6 +60,7 @@ "GitHub": "https://github.com/playcanvas" }, "plugin": [ + "./typedoc-plugin-property.mjs", "typedoc-plugin-extras", "typedoc-plugin-mdn-links", "typedoc-plugin-rename-defaults" diff --git a/typedoc-plugin-property.mjs b/typedoc-plugin-property.mjs new file mode 100644 index 0000000..bbc8e04 --- /dev/null +++ b/typedoc-plugin-property.mjs @@ -0,0 +1,140 @@ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +// eslint-disable-next-line import/no-unresolved +import { ArrayType, Converter, DeclarationReflection, IntrinsicType, ReflectionFlag, ReflectionKind, ReferenceType, UnionType } from 'typedoc'; + +/** + * Extract property types from JSDoc in a .js file. + * + * @param {string} filePath - The path to the .js file. + * @returns {Map} A map of property names to types. + */ +function getProperties(filePath) { + const data = readFileSync(resolve(process.cwd(), filePath), 'utf-8'); + const docBlocks = data.match(/\/\*\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g); + const properties = new Map(); + + if (docBlocks) { + docBlocks.forEach((block) => { + const propertyLines = block.match(/@property\s*\{[^}]+\}\s*[^*]*/g); + + if (propertyLines) { + propertyLines.forEach((line) => { + const match = line.match(/@property\s*\{([^}]+)\}\s*(\w+)/); + + if (match) { + let type = match[1].trim(); + const name = match[2].trim(); + + // Simplify complex import types. + type = type.replace(/import\(['"]([^'"]+)['"]\)\.(\w+)/g, (_, p1, p2) => p2); + + properties.set(name, type); + } + }); + } + }); + } + + return properties; +} + +/** + * This Typedoc plugin adds missing PlayCanvas API symbols to the Typedoc reflection graph. The + * symbols are missing because they are generated by `Object.defineProperty` in the PlayCanvas + * sourcebase. The TypeScript compiler is unable to detect them, either in the code or in the + * JSDoc comments (specified via \@property tags). + * + * @param {import('typedoc').Application} app - The Typedoc application. + */ +function load(app) { + const classes = new Map([ + ['ButtonComponent', './submodules/engine/src/framework/components/button/component.js'], + ['CollisionComponent', './submodules/engine/src/framework/components/collision/component.js'], + ['ElementComponent', './submodules/engine/src/framework/components/element/component.js'], + ['LightComponent', './submodules/engine/src/framework/components/light/component.js'], + ['ParticleSystemComponent', './submodules/engine/src/framework/components/particle-system/component.js'], + ['ScrollbarComponent', './submodules/engine/src/framework/components/scrollbar/component.js'], + ['ScrollViewComponent', './submodules/engine/src/framework/components/scroll-view/component.js'], + ['StandardMaterial', './submodules/engine/src/scene/materials/standard-material.js'] + ]); + + app.converter.on(Converter.EVENT_RESOLVE_BEGIN, (/** @type {import('typedoc').Context} */ context) => { + const getReference = (type) => { + const reflection = context.project.children[0].children.find(child => child.name === type && child.kind === ReflectionKind.Class); + if (!reflection) { + console.error(`Unable to find class ${type}`); + } + return reflection; + }; + + classes.forEach((filePath, className) => { + const reflection = getReference(className); + + /** + * Returns the reference type matching the specified class name. + * + * @param {string} type - The class name. + * @returns {ReferenceType} The reference type. + */ + const getReferenceType = (type) => { + const reference = getReference(type); + return reference ? new ReferenceType(type, reference, context.project) : undefined; + }; + + /** + * Returns the Typedoc type matching the specified JSDoc type. This can include a union type (|). + * + * @param {string} type - The JSDoc type string. + * @returns {import('typedoc').Type} The Typedoc type. + */ + const getType = (type) => { + if (type.includes('|')) { + const types = type.split('|'); + return new UnionType(types.map(type => getType(type))); + } + + switch (type) { + case 'null': + return new IntrinsicType('null'); + case 'boolean': + return new IntrinsicType('boolean'); + case 'number': + return new IntrinsicType('number'); + case 'number[]': + return new ArrayType(new IntrinsicType('number')); + case 'string': + return new IntrinsicType('string'); + default: + return getReferenceType(type); + } + }; + + const properties = getProperties(filePath); + + // Get just the @property definitions from the class' JSDoc block + const blockTags = reflection.comment.blockTags.filter(blockTag => blockTag.tag === '@property'); + + // Convert all @property tags on StandardMaterial to actual child properties of StandardMaterial + for (const blockTag of blockTags) { + const newProperty = new DeclarationReflection(blockTag.name, ReflectionKind.Property, reflection); + + const type = properties.get(blockTag.name); + + newProperty.type = getType(type); + + // Mark the new property as public + newProperty.setFlag(ReflectionFlag.Public, true); + + // Add the new property to the class + if (!reflection.children) { + reflection.children = []; + } + reflection.children.push(newProperty); + } + }); + }); +} + +export { load };