@playcanvas/attribute-parser 1.10.0 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -417,7 +417,7 @@ var ts = /*#__PURE__*/_mergeNamespaces({
|
|
|
417
417
|
default: typescript
|
|
418
418
|
}, [typescriptExports]);
|
|
419
419
|
|
|
420
|
-
class ParsingError{constructor(node,type,message,fix){this.node=node;this.type=type;this.message=message;this.fix=fix;}toString(){const file=this.node.getSourceFile();const fileName=file.fileName;const start=this.node.getSourceFile().getLineAndCharacterOfPosition(this.node.getStart());
|
|
420
|
+
class ParsingError{constructor(node,type,message,fix,scriptName,attributeName){this.node=node;this.type=type;this.message=message;this.fix=fix;this.scriptName=scriptName;this.attributeName=attributeName;}toString(){const file=this.node.getSourceFile();const fileName=file.fileName;const start=this.node.getSourceFile().getLineAndCharacterOfPosition(this.node.getStart());const location=`${fileName}:${start.line+1}:${start.character}`;let context="";if(this.scriptName&&this.attributeName){context=` [${this.scriptName}.${this.attributeName}]`;}else if(this.scriptName){context=` [${this.scriptName}]`;}else if(this.attributeName){context=` [${this.attributeName}]`;}return `ParsingError: ${this.message}${context} at ${location}`}}
|
|
421
421
|
|
|
422
422
|
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};const hasTag=(tag,node)=>{return !!getTag(tag,node)};const isStaticMember=member=>(typescriptExports.getCombinedModifierFlags(member)&typescriptExports.ModifierFlags.Static)!==0;
|
|
423
423
|
|
|
@@ -425,14 +425,14 @@ function createDefaultMapFromCDN(options,prefix,ts){const fsMap=new Map;const fi
|
|
|
425
425
|
|
|
426
426
|
function parseTag(input=""){input=input.replace(/['`]/g,'"').trim();if(input==="true")return true;if(input==="false")return false;const num=parseNumber(input);if(!isNaN(num))return num;try{const arr=parseStringToNumericalArray(input);if(Array.isArray(arr))return arr}catch{}try{const arr=JSON.parse(input);if(Array.isArray(arr))return arr}catch{}return input}function validateTag(value,typeAnnotation,env){const virtualFileName="/___virtual__.ts";const sourceText=`let a: ${typeAnnotation} = ${value};`;env.createFile(virtualFileName,sourceText);const languageService=env.languageService;const errors=languageService.getSemanticDiagnostics(virtualFileName);const typeErrors=errors.filter(error=>error.code===2322&&error.category===typescriptExports.DiagnosticCategory.Error);if(typeErrors.length>0){const errorMessage=typeErrors[0].messageText.toString();throw new Error(`Type Validation Error: ${errorMessage}`)}return true}
|
|
427
427
|
|
|
428
|
-
class AttributeParser{constructor(tags=new Map,env,typeSerializerMap){this.env=env;this.tagTypeAnnotations=tags;this.typeSerializerMap=typeSerializerMap;this.program=env.languageService.getProgram();this.typeChecker=this.program.getTypeChecker();}parseAttributeComment(node,errors=[],requiresAttributeTag=true){if(!requiresAttributeTag||hasTag("attribute",node)){const attribute=this.getNodeAsAttribute(node,errors);if(!attribute)return;attribute.description=node.jsDoc?.[0]?.comment||"";const jsDocTags=node.jsDoc?.[0]?.tags??[];jsDocTags.forEach(tag=>{const tagName=tag.tagName.text;let commentText=(tag.comment?.split("\n")[0]||"").trim();if(this.tagTypeAnnotations.has(tagName)){const{type,supportMessage,fix}=this.tagTypeAnnotations.get(tagName);try{const value=parseTag(commentText);if(typeof value==="string")commentText=`"${commentText}"`;validateTag(commentText,type,this.env);attribute[tagName]=value;}catch(error){let startPos=tag.getStart();let endPos=tag.getEnd();const fullText=tag.getText();const commentText=tag.comment?.split("\n")[0]?.trim()||"";const commentStart=fullText.indexOf(commentText);if(commentStart!==-1){startPos=tag.getStart()+commentStart;endPos=startPos+commentText.length;}const sourceFile=tag.getSourceFile();const startLineChar=sourceFile.getLineAndCharacterOfPosition(startPos);const endLineChar=sourceFile.getLineAndCharacterOfPosition(endPos);const edit=fix?{text:fix,title:`Fix @${tagName} tag`,range:{startLineNumber:startLineChar.line+1,startColumn:startLineChar.character+1,endLineNumber:endLineChar.line+1,endColumn:endLineChar.character+1}}:null;const errorMessage=supportMessage?.()??"The tag is invalid.";const parseError=new ParsingError(tag,`Invalid Tag '@${tagName}'`,errorMessage,edit);errors.push(parseError);}}});return attribute}}getNodeAsAttribute(node,errors=[]){const name=node.name&&typescriptExports.isIdentifier(node.name)&&node.name.text;const{type,name:typeName,array,isMixedUnion}=getType(node,this.typeChecker);const enums=this.getEnumMembers(node,errors);let value=null;if(isMixedUnion&&enums.length===0){errors.push(new ParsingError(node,"Invalid Type","Mixed literal union types (combining different primitive types like string | number) are not supported. "+'Use a union of the same primitive type (e.g., \'1 | 2 | 3\' or \'"a" | "b" | "c"\') or refactor your type.'));return}const serializer=!array&&this.typeSerializerMap.get(typeName);if(serializer){try{value=serializer(node.initializer??node,this.typeChecker);}catch(error){errors.push(error);return}}return {type,name,typeName,array,value,enum:enums}}getEnumMembers(node,errors){let typeNode=null;let members=[];if(node.type){typeNode=node;}else {const jsDocs=typescriptExports.getJSDocTags(node);const typeTag=jsDocs.find(doc=>doc.tagName.text==="type");if(typeTag&&typescriptExports.isJSDocTypeTag(typeTag)){typeNode=typeTag.typeExpression;}}const type=typeNode&&(typeNode.type.elementType??typeNode.type);if(typeNode&&typescriptExports.isTypeReferenceNode(type)){let symbol=this.typeChecker.getSymbolAtLocation(type.typeName);if(symbol&&symbol.flags&typescriptExports.SymbolFlags.Alias){symbol=this.typeChecker.getAliasedSymbol(symbol);}if(symbol){const declarations=symbol.getDeclarations();if(declarations&&declarations.length>0){declarations.forEach(declaration=>{if(typescriptExports.isEnumDeclaration(declaration)){members=declaration.members.map(member=>({[member.name.text]:member.initializer.text}));}const jsDocs=typescriptExports.getJSDocTags(declaration);const enumTag=jsDocs.find(doc=>doc.tagName.text==="enum");if(enumTag&&typescriptExports.isJSDocEnumTag(enumTag)){members=members.concat(this.getMembers(declaration.initializer,errors));}});}}}return members}getMembers(node){const members=[];if(typescriptExports.isObjectLiteralExpression(node)){node.properties.forEach(property=>{if(typescriptExports.isPropertyAssignment(property)){const isValidName=typescriptExports.isIdentifier(property.name)||typescriptExports.isStringLiteral(property.name);const name=isValidName&&property.name.text;const value=getLiteralValue(property.initializer,this.typeChecker);members.push({[name]:value});}});}return members}}
|
|
428
|
+
class AttributeParser{constructor(tags=new Map,env,typeSerializerMap){this.env=env;this.tagTypeAnnotations=tags;this.typeSerializerMap=typeSerializerMap;this.program=env.languageService.getProgram();this.typeChecker=this.program.getTypeChecker();}parseAttributeComment(node,errors=[],requiresAttributeTag=true,scriptName,attributeName){if(!requiresAttributeTag||hasTag("attribute",node)){const attribute=this.getNodeAsAttribute(node,errors,scriptName);if(!attribute)return;attribute.description=node.jsDoc?.[0]?.comment||"";const jsDocTags=node.jsDoc?.[0]?.tags??[];jsDocTags.forEach(tag=>{const tagName=tag.tagName.text;let commentText=(tag.comment?.split("\n")[0]||"").trim();if(this.tagTypeAnnotations.has(tagName)){const{type,supportMessage,fix}=this.tagTypeAnnotations.get(tagName);try{const value=parseTag(commentText);if(typeof value==="string")commentText=`"${commentText}"`;validateTag(commentText,type,this.env);attribute[tagName]=value;}catch(error){let startPos=tag.getStart();let endPos=tag.getEnd();const fullText=tag.getText();const commentText=tag.comment?.split("\n")[0]?.trim()||"";const commentStart=fullText.indexOf(commentText);if(commentStart!==-1){startPos=tag.getStart()+commentStart;endPos=startPos+commentText.length;}const sourceFile=tag.getSourceFile();const startLineChar=sourceFile.getLineAndCharacterOfPosition(startPos);const endLineChar=sourceFile.getLineAndCharacterOfPosition(endPos);const edit=fix?{text:fix,title:`Fix @${tagName} tag`,range:{startLineNumber:startLineChar.line+1,startColumn:startLineChar.character+1,endLineNumber:endLineChar.line+1,endColumn:endLineChar.character+1}}:null;const errorMessage=supportMessage?.()??"The tag is invalid.";const parseError=new ParsingError(tag,`Invalid Tag '@${tagName}'`,errorMessage,edit,scriptName,attributeName);errors.push(parseError);}}});return attribute}}getNodeAsAttribute(node,errors=[],scriptName){const name=node.name&&typescriptExports.isIdentifier(node.name)&&node.name.text;const{type,name:typeName,array,isMixedUnion}=getType(node,this.typeChecker);const enums=this.getEnumMembers(node,errors);let value=null;if(isMixedUnion&&enums.length===0){errors.push(new ParsingError(node,"Invalid Type","Mixed literal union types (combining different primitive types like string | number) are not supported. "+'Use a union of the same primitive type (e.g., \'1 | 2 | 3\' or \'"a" | "b" | "c"\') or refactor your type.',undefined,scriptName,name));return}const serializer=!array&&this.typeSerializerMap.get(typeName);if(serializer){try{value=serializer(node.initializer??node,this.typeChecker);}catch(error){if(error&&typeof error==="object"){error.scriptName=scriptName;error.attributeName=name;}errors.push(error);return}}return {type,name,typeName,array,value,enum:enums}}getEnumMembers(node,errors){let typeNode=null;let members=[];if(node.type){typeNode=node;}else {const jsDocs=typescriptExports.getJSDocTags(node);const typeTag=jsDocs.find(doc=>doc.tagName.text==="type");if(typeTag&&typescriptExports.isJSDocTypeTag(typeTag)){typeNode=typeTag.typeExpression;}}const type=typeNode&&(typeNode.type.elementType??typeNode.type);if(typeNode&&typescriptExports.isTypeReferenceNode(type)){let symbol=this.typeChecker.getSymbolAtLocation(type.typeName);if(symbol&&symbol.flags&typescriptExports.SymbolFlags.Alias){symbol=this.typeChecker.getAliasedSymbol(symbol);}if(symbol){const declarations=symbol.getDeclarations();if(declarations&&declarations.length>0){declarations.forEach(declaration=>{if(typescriptExports.isEnumDeclaration(declaration)){members=declaration.members.map(member=>({[member.name.text]:member.initializer.text}));}const jsDocs=typescriptExports.getJSDocTags(declaration);const enumTag=jsDocs.find(doc=>doc.tagName.text==="enum");if(enumTag&&typescriptExports.isJSDocEnumTag(enumTag)){members=members.concat(this.getMembers(declaration.initializer,errors));}});}}}return members}getMembers(node){const members=[];if(typescriptExports.isObjectLiteralExpression(node)){node.properties.forEach(property=>{if(typescriptExports.isPropertyAssignment(property)){const isValidName=typescriptExports.isIdentifier(property.name)||typescriptExports.isStringLiteral(property.name);const name=isValidName&&property.name.text;const value=getLiteralValue(property.initializer,this.typeChecker);members.push({[name]:value});}});}return members}}
|
|
429
429
|
|
|
430
430
|
function zipArrays(a,b){const length=Math.max(a.length,b.length);const result=new Array(length);for(let i=0;i<length;i++){result[i]=i<a.length?a[i]:b[i];}return result}
|
|
431
431
|
|
|
432
|
-
const createNewExpressionParser=(name,argProcessor,defaultArr=[])=>{return node=>{if(node.kind!==typescriptExports.SyntaxKind.NewExpression){if(typescriptExports.isPropertyAssignment(node.parent)||typescriptExports.isPropertyDeclaration(node.parent)){throw new ParsingError(node,"Invalid Property",`Property "${node.name.text}" is not a ${name}.`)}return defaultArr}const args=argProcessor(node.arguments);return zipArrays(args,defaultArr)}};const parseNumberArguments=args=>args.map(arg=>parseFloatNode(arg,true));const createNumberArgumentParser=(name,defaultArr=[])=>createNewExpressionParser(name,parseNumberArguments,defaultArr);const createArrayArgumentParser=(name,defaultArr=[])=>createNewExpressionParser(name,parseArrayLiteral,defaultArr);const SUPPORTED_INITIALIZABLE_TYPE_NAMES=new Map([["Curve",createArrayArgumentParser("Curve")],["Vec2",createNumberArgumentParser("Vec2",[0,0])],["Vec3",createNumberArgumentParser("Vec3",[0,0,0])],["Vec4",createNumberArgumentParser("Vec4",[0,0,0,0])],["Color",createNumberArgumentParser("Color",[1,1,1,1])],["number",getLiteralValue],["string",getLiteralValue],["boolean",getLiteralValue]]);const SUPPORTED_PLAYCANVAS_TYPES=new Set(["Curve","Asset","Entity","Vec2","Vec3","Vec4","Color"]);const isSupportedPlayCanvasNode=node=>{return typescriptExports.isClassDeclaration(node)&&SUPPORTED_PLAYCANVAS_TYPES.has(node.name.text)};const EDITOR_ASSET_TYPES=["animation","animstategraph","audio","bundle","binary","container","cubemap","css","font","folder","json","html","gsplat","material","model","render","scene","script","shader","sprite","template","text","texture","textureAtlas","wasm"];const resourceTypesAsUnion=EDITOR_ASSET_TYPES.map(type=>`"${type}"`).join("|");const SUPPORTED_BLOCK_TAGS=new Map([["resource",{type:resourceTypesAsUnion,supportMessage:()=>`@resource is malformed or invalid. The tag should be formatted as "@resource ${resourceTypesAsUnion}".`,fix:"texture"}],["color",{type:'"r"|"rg"|"rgb"|"rgba"',supportMessage:()=>'@color is malformed or invalid. The tag should be formatted as "@color r", "@color rg", "@color rgb", or "@color rgba".',fix:"rgb"}],["curves",{type:'["x"] | ["x", "y"] | ["x", "y", "z"] | ["x", "z"] | ["y"] | ["y", "z"] | ["z"]',supportMessage:()=>'@curves is malformed or invalid. The tag should be formatted as "@curves x", "@curves x y", "@curves x y z", "@curves x z", "@curves y", "@curves y z", or "@curves z".',fix:'["x"]'}],["range",{type:"number[]",supportMessage:()=>'@range is malformed or invalid. The tag should be formatted as "@range [0, 100]".',fix:"[0, 100]"}],["placeholder",{type:"string",supportMessage:()=>'@placeholder is malformed or invalid. The tag should be formatted as "@placeholder "My Placeholder"".',fix:'"My Placeholder"'}],["precision",{type:"number",supportMessage:()=>'@precision is malformed or invalid. The tag should be formatted as "@precision 0.1".',fix:"0.1"}],["size",{type:"number",supportMessage:()=>'@size is malformed or invalid. The tag should be formatted as "@size 100".',fix:"100"}],["step",{type:"number",supportMessage:()=>'@step is malformed or invalid. The tag should be formatted as "@step 1".',fix:"1"}],["title",{type:"string",supportMessage:()=>'@title is malformed or invalid. The tag should be formatted as "@title "My Attribute"".',fix:'"My Attribute"'}],["default",{type:"any"}],["enabledif",{type:"any"}],["visibleif",{type:"any"}]]);const mapAttributesToOutput=attribute=>{attribute.type=attribute.typeName.toLowerCase();if(attribute.range){if(attribute.range.length>0){attribute.min=attribute.range[0];if(attribute.range.length>1){attribute.max=attribute.range[1];}}delete attribute.range;}if(typeof attribute.attributes=="object"){attribute.type="json";attribute.schema=Object.keys(attribute.attributes).map(key=>{return attribute.attributes[key]});delete attribute.attributes;}if(attribute.type==="color"){attribute.type="rgba";}if(attribute.resource){attribute.assetType=attribute.resource;delete attribute.resource;}if(attribute.enum.length===0)delete attribute.enum;if(attribute.value===undefined){if(attribute.type==="string")attribute.value="";if(attribute.type==="number")attribute.value=0;if(attribute.type==="boolean")attribute.value=false;}if(attribute.value!==undefined)attribute.default=attribute.default??attribute.value;if(attribute.type==="curve"&&Array.isArray(attribute.value)&&attribute.value.length===0){delete attribute.default;}if(attribute.enabledif?.startsWith("{")&&attribute.enabledif?.endsWith("}")){attribute.enabledif=attribute.enabledif.slice(1,-1);}if(attribute.visibleif?.startsWith("{")&&attribute.visibleif?.endsWith("}")){attribute.visibleif=attribute.visibleif.slice(1,-1);}delete attribute.typeName;delete attribute.value;return attribute};class ScriptParser{constructor(env,maxDepth=2){if(!env){throw new Error("A TypeScript Environment must be provided to the ScriptParser.")}this.env=env;this.program=env.languageService.getProgram();this.typeChecker=this.program.getTypeChecker();this.maxDepth=maxDepth;const playcanvasTypesFile=this.program.getSourceFile("./playcanvas.d.ts");if(!playcanvasTypesFile){throw new Error("The PlayCanvas types file could not be found.")}const allSupportedTypes=flatMapAnyNodes(playcanvasTypesFile,isSupportedPlayCanvasNode).map(node=>this.typeChecker.getTypeAtLocation(node));allSupportedTypes.push(this.typeChecker.getStringType());allSupportedTypes.push(this.typeChecker.getNumberType());allSupportedTypes.push(this.typeChecker.getBooleanType());this.typeSerializerMap=new Map;this.allSupportedTypesSet=new Set(allSupportedTypes);allSupportedTypes.forEach(type=>{const typeName=this.typeChecker.typeToString(type);const serializer=SUPPORTED_INITIALIZABLE_TYPE_NAMES.get(typeName);if(serializer){this.typeSerializerMap.set(typeName,serializer);}});this.attributeParser=new AttributeParser(SUPPORTED_BLOCK_TAGS,env,this.typeSerializerMap);}validateProgram(errors=[]){const compilerDiagnostics=this.program.getSyntacticDiagnostics();if(compilerDiagnostics.length>0){for(const diagnostic of compilerDiagnostics){const message=typescriptExports.flattenDiagnosticMessageText(diagnostic.messageText,"\n");if(diagnostic.file){const location=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);errors.push({message,file:diagnostic.file.fileName,line:location.line+1,column:location.character+1,length:diagnostic.length});}else {errors.push({message});}}}return errors.length===0}extractAttributes(node,opts={}){const attributes=opts.attributes||{};const errors=opts.errors??[];const requiresAttributeTag=opts.requiresAttributeTag??true;const depth=opts.depth??0;if(depth>10){return attributes}const members=getJSDocTags(node,this.typeChecker);for(const{member,memberName}of members){const attributeMetadata=this.attributeParser.parseAttributeComment(member,errors,requiresAttributeTag);if(attributeMetadata){const{type,typeName}=attributeMetadata;const initializer=member?.initializer;if(typeName==="any"){const supportedTypes=Array.from(SUPPORTED_INITIALIZABLE_TYPE_NAMES.keys()).join(", ");let errorMessage;if(member.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}.`;}const error=new ParsingError(member,"Invalid Type",errorMessage);errors.push(error);continue}else if(!this.allSupportedTypesSet.has(type)&&attributeMetadata.enum.length===0){const symbol=type.aliasSymbol||type.symbol;const typeNode=symbol?.valueDeclaration||symbol.declarations?.[0];if(!typeNode){continue}const typeIsJSDocTypeDef=typescriptExports.isJSDocTypedefTag(typeNode);const typeIsInterface=hasTag("interface",typeNode)||typescriptExports.isInterfaceDeclaration(typeNode);const isInitialized=!!initializer&&typescriptExports.isObjectLiteralExpression(initializer);const isJSDocTypeLiteral=type.symbol.name==="__type";if(!isInitialized&&!typeIsInterface&&!typeIsJSDocTypeDef&&!isJSDocTypeLiteral){let error;const typeTag=getTag("type",member);const supportedTypes=Array.from(SUPPORTED_INITIALIZABLE_TYPE_NAMES.keys()).join(", ");if(typeTag){error=new ParsingError(typeTag.typeExpression,"Invalid Type",`"${typeTag.typeExpression.getText()}" is not a valid attribute type. An attribute should be one of the following: ${supportedTypes}
|
|
432
|
+
const createNewExpressionParser=(name,argProcessor,defaultArr=[])=>{return node=>{if(node.kind!==typescriptExports.SyntaxKind.NewExpression){if(typescriptExports.isPropertyAssignment(node.parent)||typescriptExports.isPropertyDeclaration(node.parent)){throw new ParsingError(node,"Invalid Property",`Property "${node.name.text}" is not a ${name}.`)}return defaultArr}const args=argProcessor(node.arguments);return zipArrays(args,defaultArr)}};const parseNumberArguments=args=>args.map(arg=>parseFloatNode(arg,true));const createNumberArgumentParser=(name,defaultArr=[])=>createNewExpressionParser(name,parseNumberArguments,defaultArr);const createArrayArgumentParser=(name,defaultArr=[])=>createNewExpressionParser(name,parseArrayLiteral,defaultArr);const SUPPORTED_INITIALIZABLE_TYPE_NAMES=new Map([["Curve",createArrayArgumentParser("Curve")],["Vec2",createNumberArgumentParser("Vec2",[0,0])],["Vec3",createNumberArgumentParser("Vec3",[0,0,0])],["Vec4",createNumberArgumentParser("Vec4",[0,0,0,0])],["Color",createNumberArgumentParser("Color",[1,1,1,1])],["number",getLiteralValue],["string",getLiteralValue],["boolean",getLiteralValue]]);const SUPPORTED_PLAYCANVAS_TYPES=new Set(["Curve","Asset","Entity","Vec2","Vec3","Vec4","Color"]);const isSupportedPlayCanvasNode=node=>{return typescriptExports.isClassDeclaration(node)&&SUPPORTED_PLAYCANVAS_TYPES.has(node.name.text)};const EDITOR_ASSET_TYPES=["animation","animstategraph","audio","bundle","binary","container","cubemap","css","font","folder","json","html","gsplat","material","model","render","scene","script","shader","sprite","template","text","texture","textureAtlas","wasm"];const resourceTypesAsUnion=EDITOR_ASSET_TYPES.map(type=>`"${type}"`).join("|");const SUPPORTED_BLOCK_TAGS=new Map([["resource",{type:resourceTypesAsUnion,supportMessage:()=>`@resource is malformed or invalid. The tag should be formatted as "@resource ${resourceTypesAsUnion}".`,fix:"texture"}],["color",{type:'"r"|"rg"|"rgb"|"rgba"',supportMessage:()=>'@color is malformed or invalid. The tag should be formatted as "@color r", "@color rg", "@color rgb", or "@color rgba".',fix:"rgb"}],["curves",{type:'["x"] | ["x", "y"] | ["x", "y", "z"] | ["x", "z"] | ["y"] | ["y", "z"] | ["z"]',supportMessage:()=>'@curves is malformed or invalid. The tag should be formatted as "@curves x", "@curves x y", "@curves x y z", "@curves x z", "@curves y", "@curves y z", or "@curves z".',fix:'["x"]'}],["range",{type:"number[]",supportMessage:()=>'@range is malformed or invalid. The tag should be formatted as "@range [0, 100]".',fix:"[0, 100]"}],["placeholder",{type:"string",supportMessage:()=>'@placeholder is malformed or invalid. The tag should be formatted as "@placeholder "My Placeholder"".',fix:'"My Placeholder"'}],["precision",{type:"number",supportMessage:()=>'@precision is malformed or invalid. The tag should be formatted as "@precision 0.1".',fix:"0.1"}],["size",{type:"number",supportMessage:()=>'@size is malformed or invalid. The tag should be formatted as "@size 100".',fix:"100"}],["step",{type:"number",supportMessage:()=>'@step is malformed or invalid. The tag should be formatted as "@step 1".',fix:"1"}],["title",{type:"string",supportMessage:()=>'@title is malformed or invalid. The tag should be formatted as "@title "My Attribute"".',fix:'"My Attribute"'}],["default",{type:"any"}],["enabledif",{type:"any"}],["visibleif",{type:"any"}]]);const mapAttributesToOutput=attribute=>{attribute.type=attribute.typeName.toLowerCase();if(attribute.range){if(attribute.range.length>0){attribute.min=attribute.range[0];if(attribute.range.length>1){attribute.max=attribute.range[1];}}delete attribute.range;}if(typeof attribute.attributes=="object"){attribute.type="json";attribute.schema=Object.keys(attribute.attributes).map(key=>{return attribute.attributes[key]});delete attribute.attributes;}if(attribute.type==="color"){attribute.type="rgba";}if(attribute.resource){attribute.assetType=attribute.resource;delete attribute.resource;}if(attribute.enum.length===0)delete attribute.enum;if(attribute.value===undefined){if(attribute.type==="string")attribute.value="";if(attribute.type==="number")attribute.value=0;if(attribute.type==="boolean")attribute.value=false;}if(attribute.value!==undefined)attribute.default=attribute.default??attribute.value;if(attribute.type==="curve"&&Array.isArray(attribute.value)&&attribute.value.length===0){delete attribute.default;}if(attribute.enabledif?.startsWith("{")&&attribute.enabledif?.endsWith("}")){attribute.enabledif=attribute.enabledif.slice(1,-1);}if(attribute.visibleif?.startsWith("{")&&attribute.visibleif?.endsWith("}")){attribute.visibleif=attribute.visibleif.slice(1,-1);}delete attribute.typeName;delete attribute.value;return attribute};class ScriptParser{constructor(env,maxDepth=2){if(!env){throw new Error("A TypeScript Environment must be provided to the ScriptParser.")}this.env=env;this.program=env.languageService.getProgram();this.typeChecker=this.program.getTypeChecker();this.maxDepth=maxDepth;const playcanvasTypesFile=this.program.getSourceFile("./playcanvas.d.ts");if(!playcanvasTypesFile){throw new Error("The PlayCanvas types file could not be found.")}const allSupportedTypes=flatMapAnyNodes(playcanvasTypesFile,isSupportedPlayCanvasNode).map(node=>this.typeChecker.getTypeAtLocation(node));allSupportedTypes.push(this.typeChecker.getStringType());allSupportedTypes.push(this.typeChecker.getNumberType());allSupportedTypes.push(this.typeChecker.getBooleanType());this.typeSerializerMap=new Map;this.allSupportedTypesSet=new Set(allSupportedTypes);allSupportedTypes.forEach(type=>{const typeName=this.typeChecker.typeToString(type);const serializer=SUPPORTED_INITIALIZABLE_TYPE_NAMES.get(typeName);if(serializer){this.typeSerializerMap.set(typeName,serializer);}});this.attributeParser=new AttributeParser(SUPPORTED_BLOCK_TAGS,env,this.typeSerializerMap);}validateProgram(errors=[]){const compilerDiagnostics=this.program.getSyntacticDiagnostics();if(compilerDiagnostics.length>0){for(const diagnostic of compilerDiagnostics){const message=typescriptExports.flattenDiagnosticMessageText(diagnostic.messageText,"\n");if(diagnostic.file){const location=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);errors.push({message,file:diagnostic.file.fileName,line:location.line+1,column:location.character+1,length:diagnostic.length});}else {errors.push({message});}}}return errors.length===0}extractAttributes(node,opts={}){const attributes=opts.attributes||{};const errors=opts.errors??[];const requiresAttributeTag=opts.requiresAttributeTag??true;const depth=opts.depth??0;const scriptName=opts.scriptName??undefined;if(depth>10){return attributes}const members=getJSDocTags(node,this.typeChecker);for(const{member,memberName}of members){const attributeMetadata=this.attributeParser.parseAttributeComment(member,errors,requiresAttributeTag,scriptName,memberName);if(attributeMetadata){const{type,typeName}=attributeMetadata;const initializer=member?.initializer;if(typeName==="any"){const supportedTypes=Array.from(SUPPORTED_INITIALIZABLE_TYPE_NAMES.keys()).join(", ");let errorMessage;if(member.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}.`;}const error=new ParsingError(member,"Invalid Type",errorMessage,node.name.getText(),memberName,undefined);errors.push(error);continue}else if(!this.allSupportedTypesSet.has(type)&&attributeMetadata.enum.length===0){const symbol=type.aliasSymbol||type.symbol;const typeNode=symbol?.valueDeclaration||symbol.declarations?.[0];if(!typeNode){continue}const typeIsJSDocTypeDef=typescriptExports.isJSDocTypedefTag(typeNode);const typeIsInterface=hasTag("interface",typeNode)||typescriptExports.isInterfaceDeclaration(typeNode);const isInitialized=!!initializer&&typescriptExports.isObjectLiteralExpression(initializer);const isJSDocTypeLiteral=type.symbol.name==="__type";if(!isInitialized&&!typeIsInterface&&!typeIsJSDocTypeDef&&!isJSDocTypeLiteral){let error;const typeTag=getTag("type",member);const supportedTypes=Array.from(SUPPORTED_INITIALIZABLE_TYPE_NAMES.keys()).join(", ");if(typeTag){error=new ParsingError(typeTag.typeExpression,"Invalid Type",`"${typeTag.typeExpression.getText()}" is not a valid attribute type. An attribute should be one of the following: ${supportedTypes}`,undefined,node.name.getText(),memberName);}else if(initializer){error=new ParsingError(initializer,"Invalid Type",`"${initializer.getText()}" is not a valid attribute type. An attribute should be one of the following: ${supportedTypes}`,undefined,node.name.getText(),memberName);}else {error=new ParsingError(member,"Invalid Type",`Attribute is an invalid type. An attribute should be one of the following: ${supportedTypes}`,undefined,node.name.getText(),memberName);}errors.push(error);continue}const members=typeNode.members??typeNode.properties??Array.from(type.symbol.members).map(arr=>arr[1].declarations[0])??[];let commentAttributes;if(typeIsInterface||isInitialized){commentAttributes=this.extractAttributes(typeNode,{errors,requiresAttributeTag:false,depth:depth+1,scriptName});}const nestedAttributes=members.reduce((attributes,prop)=>{const attribute=this.attributeParser.getNodeAsAttribute(prop,errors,scriptName);if(attribute&&this.allSupportedTypesSet.has(attribute.type)){attributes[attribute.name]=mapAttributesToOutput(attribute);}return attributes},{});const mergedAttributes={...nestedAttributes,...commentAttributes};attributeMetadata.attributes=mergedAttributes;}attributes[memberName]=mapAttributesToOutput(attributeMetadata);}}return attributes}}
|
|
433
433
|
|
|
434
434
|
const toLowerCamelCase=str=>str[0].toLowerCase()+str.substring(1);const SUPPORTED_ATTRIBUTE_TYPES=new Set(["Curve","Vec2","Vec3","Vec4","Color","number","string","boolean"]);const COMPILER_OPTIONS={noLib:true,strict:false,skipLibCheck:true,target:typescriptExports.ScriptTarget.ES2023,module:typescriptExports.ModuleKind.CommonJS,checkJs:true,allowJs:true,baseUrl:"./",paths:{"playcanvas":["/playcanvas.js"]}};class JSDocParser{async init(libPath){if(this._env){return this}let fsMap;if(!libPath){fsMap=await createDefaultMapFromNodeModules();}else if(libPath.endsWith(".d.ts")){const types=await fetch(libPath).then(r=>r.text());fsMap=new Map([["/lib.d.ts",types]]);}else {fsMap=await createDefaultMapFromCDN(COMPILER_OPTIONS,libPath,ts);}const system=createSystem(fsMap);this._env=createVirtualTypeScriptEnvironment(system,Array.from(fsMap.keys()),ts,COMPILER_OPTIONS);return this}updateProgram(newFiles=[],removedFiles=[]){const errors=[];newFiles.forEach(([filePath,fileContent])=>{if(this._env.getSourceFile(filePath)){this._env.updateFile(filePath,fileContent);}else {this._env.createFile(filePath,fileContent);}});removedFiles.forEach(filePath=>{this._env.deleteFile(filePath);});this.parser=new ScriptParser(this._env);this.program=this._env.languageService.getProgram();this.parser.validateProgram(errors);this._errors=errors;return errors}getAllEsmScripts(fileName,errors=[]){const typeChecker=this.program.getTypeChecker();const pcTypes=this.program.getSourceFile("/playcanvas.d.ts");if(!pcTypes){throw new Error("PlayCanvas Types must be supplied")}const esmScripts=new Map;const sourceFile=this.program.getSourceFile(fileName);if(sourceFile===undefined){return esmScripts}if(sourceFile===null){throw new Error(`Source file ${fileName} not found`)}const nodes=getExportedNodes(this.program,sourceFile);const esmScriptClass=pcTypes.statements.find(node=>node.kind===typescriptExports.SyntaxKind.ClassDeclaration&&node.name.text==="Script")?.symbol;const isScriptNameMember=member=>typescriptExports.isPropertyDeclaration(member)&&typescriptExports.isIdentifier(member.name)&&member.name.text==="scriptName"&&isStaticMember(member)&&typescriptExports.isStringLiteral(member.initializer);nodes.forEach((node,name)=>{if(isAliasedClassDeclaration(node,typeChecker)&&inheritsFrom(node,typeChecker,esmScriptClass)){const scriptNameMember=node.members.find(isScriptNameMember);let finalName;if(scriptNameMember){finalName=scriptNameMember.initializer.text;}else {finalName=toLowerCamelCase(name);const insertOffset=node.members.pos;const insertPos=node.getSourceFile().getLineAndCharacterOfPosition(insertOffset);errors.push(new ParsingError(node,"Missing Script Name","Scripts should have a static scriptName member that identifies the script.",{title:"Add scriptName",text:`
|
|
435
435
|
static scriptName = '${finalName}';
|
|
436
|
-
`,range:{startLineNumber:insertPos.line+1,startColumn:insertPos.character+1,endLineNumber:insertPos.line+1,endColumn:insertPos.character+1}}));}esmScripts.set(finalName,node);}});return esmScripts}parseAttributes(fileName){const results={};const errors=[...this._errors];if(errors.length>0){return [results,errors]}const nodes=this.getAllEsmScripts(fileName,errors);nodes.forEach((node,name)=>{const opts=results[name]={attributes:{},errors:[]};this.parser.extractAttributes(node,opts);});return [results,errors]}getAttributes(fileName){const results={};const errors=[...this._errors];if(errors.length>0){return [results,errors]}const typeChecker=this.program.getTypeChecker();const esmScripts=this.getAllEsmScripts(fileName,errors);const[attributes]=this.parseAttributes(fileName);const scripts=[];for(const[scriptName,node]of esmScripts){const members=[];for(const member of node.members){if(member.kind===typescriptExports.SyntaxKind.PropertyDeclaration&&!isStaticMember(member)){members.push(member);}}scripts.push({scriptName,members});errors.push(...attributes[scriptName]?.errors??[]);}for(const{scriptName,members}of scripts){const localMembers=[];for(const member of members){const name=member.name.getText();const attribute=attributes[scriptName].attributes[name];const isAttribute=!!attribute;const type=getType(member,typeChecker);if(!SUPPORTED_ATTRIBUTE_TYPES.has(type.name)){continue}const namePos=typescriptExports.getLineAndCharacterOfPosition(member.getSourceFile(),member.name.getStart());const jsdocNode=member.jsDoc&&member.jsDoc[member.jsDoc.length-1];const jsdocPos=jsdocNode?typescriptExports.getLineAndCharacterOfPosition(member.getSourceFile(),jsdocNode.getStart()):null;localMembers.push({name,isAttribute,type:type.name+(type.array?"[]":""),start:jsdocNode?{line:jsdocPos.line+1,column:jsdocPos.character+1}:{line:namePos.line+1,column:namePos.character+1},end:{line:namePos.line+1,column:namePos.character+
|
|
436
|
+
`,range:{startLineNumber:insertPos.line+1,startColumn:insertPos.character+1,endLineNumber:insertPos.line+1,endColumn:insertPos.character+1}},finalName));}esmScripts.set(finalName,node);}});return esmScripts}parseAttributes(fileName){const results={};const errors=[...this._errors];if(errors.length>0){return [results,errors]}const nodes=this.getAllEsmScripts(fileName,errors);nodes.forEach((node,name)=>{const opts=results[name]={attributes:{},errors:[]};this.parser.extractAttributes(node,{...opts,scriptName:name});});return [results,errors]}getAttributes(fileName){const results={};const errors=[...this._errors];if(errors.length>0){return [results,errors]}const typeChecker=this.program.getTypeChecker();const esmScripts=this.getAllEsmScripts(fileName,errors);const[attributes]=this.parseAttributes(fileName);const scripts=[];for(const[scriptName,node]of esmScripts){const members=[];for(const member of node.members){if(member.kind===typescriptExports.SyntaxKind.PropertyDeclaration&&!isStaticMember(member)){members.push(member);}}scripts.push({scriptName,members});errors.push(...attributes[scriptName]?.errors??[]);}for(const{scriptName,members}of scripts){const localMembers=[];for(const member of members){const name=member.name.getText();const attribute=attributes[scriptName].attributes[name];const isAttribute=!!attribute;const type=getType(member,typeChecker);if(!SUPPORTED_ATTRIBUTE_TYPES.has(type.name)){continue}const namePos=typescriptExports.getLineAndCharacterOfPosition(member.getSourceFile(),member.name.getStart());const jsdocNode=member.jsDoc&&member.jsDoc[member.jsDoc.length-1];const jsdocPos=jsdocNode?typescriptExports.getLineAndCharacterOfPosition(member.getSourceFile(),jsdocNode.getStart()):null;localMembers.push({name,isAttribute,type:type.name+(type.array?"[]":""),start:jsdocNode?{line:jsdocPos.line+1,column:jsdocPos.character+1}:{line:namePos.line+1,column:namePos.character+1},end:{line:namePos.line+1,column:namePos.character+1}});}results[scriptName]=localMembers;}return [results,errors]}constructor(){this._errors=[];}}
|
|
437
437
|
|
|
438
438
|
export { JSDocParser };
|
package/package.json
CHANGED
|
@@ -28,17 +28,20 @@ export class AttributeParser {
|
|
|
28
28
|
* @param {ts.Node} node - The node to parse
|
|
29
29
|
* @param {ParsingError[]} errors - An array to store any parsing errors
|
|
30
30
|
* @param {boolean} requiresAttributeTag - Whether the comment must have an attribute tag
|
|
31
|
+
* @param {string} scriptName - The name of the script this node belongs to
|
|
32
|
+
* @param {string} attributeName - The name of the attribute this node belongs to
|
|
31
33
|
* @returns {*} - The extracted metadata or null if no metadata was found
|
|
32
34
|
*/
|
|
33
|
-
parseAttributeComment(node: ts.Node, errors
|
|
35
|
+
parseAttributeComment(node: ts.Node, errors: ParsingError[], requiresAttributeTag: boolean, scriptName: string, attributeName: string): any;
|
|
34
36
|
/**
|
|
35
37
|
* Extract the public members and their values from a TypeScript node.
|
|
36
38
|
*
|
|
37
39
|
* @param {ts.Node} node - The node to parse
|
|
38
40
|
* @param {ParsingError[]} errors - An array to store any parsing errors
|
|
41
|
+
* @param {string} [scriptName] - The name of the script this node belongs to
|
|
39
42
|
* @returns {*} - The extracted members
|
|
40
43
|
*/
|
|
41
|
-
getNodeAsAttribute(node: ts.Node, errors?: ParsingError[]): any;
|
|
44
|
+
getNodeAsAttribute(node: ts.Node, errors?: ParsingError[], scriptName?: string): any;
|
|
42
45
|
/**
|
|
43
46
|
* Extracts the members of an enum from a TypeScript node.
|
|
44
47
|
*
|
|
@@ -10,13 +10,17 @@
|
|
|
10
10
|
* @param {string} type - The type of the error
|
|
11
11
|
* @param {string} message - The description of the error
|
|
12
12
|
* @param {Fix} [fix] - The fix for the error
|
|
13
|
+
* @param {string} [scriptName] - The name of the script this error belongs to (required)
|
|
14
|
+
* @param {string} [attributeName] - The name of the attribute this error belongs to (optional)
|
|
13
15
|
*/
|
|
14
16
|
export class ParsingError {
|
|
15
|
-
constructor(node: any, type: any, message: any, fix: any);
|
|
17
|
+
constructor(node: any, type: any, message: any, fix: any, scriptName: any, attributeName: any);
|
|
16
18
|
node: any;
|
|
17
19
|
type: any;
|
|
18
20
|
message: any;
|
|
19
21
|
fix: any;
|
|
22
|
+
scriptName: any;
|
|
23
|
+
attributeName: any;
|
|
20
24
|
toString(): string;
|
|
21
25
|
}
|
|
22
26
|
export type Fix = {
|
|
@@ -27,6 +27,7 @@ export class ScriptParser {
|
|
|
27
27
|
* @property {ParsingError[]} [errors] - The array to store parsing errors
|
|
28
28
|
* @property {boolean} [requiresAttributeTag] - Whether the attribute requires an @attribute tag
|
|
29
29
|
* @property {number} [depth] - The current recursion depth
|
|
30
|
+
* @property {string} [scriptName] - The current script name for context
|
|
30
31
|
*/
|
|
31
32
|
/**
|
|
32
33
|
* Extracts Script Attributes from JSDoc style tag from a AST node
|