diff --git a/package.json b/package.json index 0f8c3cf..8cf5d48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tsd-ext-type-gen", - "version": "1.0.4", + "version": "1.0.5", "description": "TypeScript definitions generator that parses script APIs from Defold extensions", "repository": { "type": "git", diff --git a/src/xtgen.ts b/src/xtgen.ts index 0d8f0cd..2ca35ad 100644 --- a/src/xtgen.ts +++ b/src/xtgen.ts @@ -16,6 +16,8 @@ const HEADER = `/** @noSelfInFile */ // Invalid names in TypeScript const INVALID_NAMES = [ + 'any', + 'boolean', 'break', 'case', 'catch', @@ -27,6 +29,7 @@ const INVALID_NAMES = [ 'delete', 'do', 'else', + 'enum', 'export', 'extends', 'false', @@ -34,12 +37,20 @@ const INVALID_NAMES = [ 'for', 'function', 'if', + 'implements', 'import', 'in', 'instanceof', + 'interface', + 'let', 'new', 'null', + 'package', + 'private', + 'protected', + 'public', 'return', + 'static', 'super', 'switch', 'this', @@ -47,6 +58,7 @@ const INVALID_NAMES = [ 'true', 'try', 'typeof', + 'undefined', 'var', 'void', 'while', @@ -291,15 +303,31 @@ function isApiFunc( } // Sanitizes name -function getName(name: string) { - let modifiedName = name.replace('...', 'args'); +function getName(name: string, isParam: boolean) { + let modifiedName = String(name); + // Check against the reserved keywords in TypeScript if (INVALID_NAMES.includes(modifiedName)) { modifiedName = modifiedName + '_'; } - // Sanitize type name, allow alpha-numeric and underscore + + if (isParam) { + // Special case: arguments + modifiedName = modifiedName.replace(/^\.\.\.$/, 'args'); + // Special case: Lua's `self` variable + modifiedName = modifiedName.replace(/^self$/, 'this'); + } + + // Sanitize type name: allow only alpha-numeric, underscore, dollar sign modifiedName = modifiedName.replace(/[^a-zA-Z0-9_$]/g, '_'); - if (modifiedName !== name) { + + // If the first character is a number, add a dollar sign to start + if (/^\d/.test(modifiedName)) { + modifiedName = '$' + modifiedName; + } + + // If we're modifying a function name, not a parameter, give a warning + if (!isParam && modifiedName !== name) { console.warn(`Modifying invalid name "${name}" to "${modifiedName}"`); } @@ -331,7 +359,7 @@ function getType( } function sanitizeForComment(str: string) { - return str.replace(/\*\//g, ''); + return str.replace(/\*\//g, '*\\/'); } // Transforms and sanitizes descriptions @@ -397,7 +425,7 @@ function getReturnComments( function getParamComments(parameters: ScriptApiParameter[], newDesc: string) { parameters.forEach((param) => { - const name = param.name ? getName(param.name) : ''; + const name = param.name ? getName(param.name, true) : ''; if (name) { newDesc += `\n * @param`; if (param.type) { @@ -453,7 +481,7 @@ function generateTableDefinition( details: ScriptDetails, start = false, ): string { - const name = entry.name ? getName(entry.name) : DEFAULT_NAME_IF_BLANK; + const name = entry.name ? getName(entry.name, false) : DEFAULT_NAME_IF_BLANK; let tableDeclaration = `export namespace ${name} {\n`; if (start) { tableDeclaration = details.isLua @@ -482,13 +510,15 @@ function generateFunctionDefinition( return `(${parameters}) => ${returnType}`; } else { const comment = getComments(entry); - const name = entry.name ? getName(entry.name) : DEFAULT_NAME_IF_BLANK; + const name = entry.name + ? getName(entry.name, false) + : DEFAULT_NAME_IF_BLANK; return `${comment}export function ${name}(${parameters}): ${returnType};\n`; } } function getParameterDefinition(param: ScriptApiParameter): string { - const name = param.name ? getName(param.name) : DEFAULT_NAME_IF_BLANK; + const name = param.name ? getName(param.name, true) : DEFAULT_NAME_IF_BLANK; const optional = param.optional ? '?' : ''; let type = getType(param.type, 'param'); @@ -528,7 +558,7 @@ function getReturnType( // Function to generate TypeScript definitions for ScriptApiEntry function generateEntryDefinition(entry: ScriptApiEntry): string { - const name = entry.name ? getName(entry.name) : DEFAULT_NAME_IF_BLANK; + const name = entry.name ? getName(entry.name, false) : DEFAULT_NAME_IF_BLANK; const varType = isAllUppercase(name) ? 'const' : 'let'; const type = getType(entry.type, 'return'); const comment = getComments(entry); @@ -546,7 +576,7 @@ function generateTypeScriptDefinitions( api.forEach((entry) => { // Handle nested properties - if (entry.name && entry.name.includes('.')) { + if (typeof entry.name === 'string' && entry.name.includes('.')) { const namePieces = entry.name.split('.'); const entryNamespace = namePieces[0]; const entryName = namePieces[1]; diff --git a/tests/fuzz.script_api b/tests/fuzz.script_api index 8c4f9ea..6a11838 100644 --- a/tests/fuzz.script_api +++ b/tests/fuzz.script_api @@ -1,39 +1,78 @@ -- name: "Fuzz!ngExtens!on" - type: "t@ble" - desc: "Extension for Fuzz!ng JavaScript Code" +- name: Fuzz!ngExtens!on + type: t@ble + desc: Extension for Fuzz!ng JavaScript Code members: - - name: "fuzzC*de" - type: "function" - desc: "Fuzzes the provided JavaScript code." + - name: fuzzC*de + type: function + desc: Fuzzes the provided JavaScript code. parameters: - - name: "c*de" - type: "str!ng" - desc: "The JavaScript code to be fuzzed. Must be a valid str!ng." + - name: ... + desc: The JavaScript code to be fuzzed. returns: - - name: "fuzz?dC*de" - type: "s*ring" - desc: "The fuzzed JavaScript code." + - name: fuzz?dC*de + type: s*ring + desc: The fuzzed JavaScript code. examples: - - desc: "Fuzz!ng a sample JavaScript code" - - - name: "/*gen&rate!nval!dInput" - type: "function" - desc: "Generates an !nval!d JavaScript !nput for test!ng." + - desc: Fuzz!ng a sample JavaScript code + - name: /*gen&rate!nval!dInput + type: function + desc: Generates an !nval!d JavaScript !nput for test!ng. parameters: - - name: "//c*de" - type: "str!ng|n&mber|h@sh" - desc: "Optional JavaScript code to be fuzzed. Must be a valid str!ng." - - name: "t3+st" - type: "s=r!ng" - - name: "f%zz" - type: "str&ng" - desc: "/* */ //" + - name: self + type: any + - name: //c*de + type: str!ng|n&mber|h@sh + desc: Optional JavaScript code to be fuzzed! + - name: t3+st + type: s=r!ng + - name: f%zz + type: str&ng + desc: /* */ // returns: - - name: "!nval!dInput" - type: "st.r!ng" - desc: "The generated !nval!d JavaScript !nput." + - name: '!nval!dInput' + type: st.r!ng + desc: The generated !nval!d JavaScript !nput. examples: - - desc: "Generating !nval!d JavaScript input" - - - name: "/*C&NST@NT" - type: "f#nct!on" + - desc: Generating !nval!d JavaScript input + - name: /*C&NST@NT + type: f#nct!on + - name: 1invalid + - name: class + - name: const + - name: break + - name: let's_try_this + - name: 123_variable + - name: true + - name: false + - name: function + - name: typeof + - name: undefined + - name: for + - name: in + - name: instanceof + - name: switch + - name: case + - name: try + - name: catch + - name: throw + - name: package + - name: private + - name: protected + - name: public + - name: return + - name: static + - name: super + - name: this + - name: while + - name: with + - name: yield + - name: delete + - name: debugger + - name: else + - name: enum + - name: export + - name: extends + - name: finally + - name: if + - name: implements + - name: import diff --git a/tests/game.project b/tests/game.project index 12f76cd..48c41f0 100644 --- a/tests/game.project +++ b/tests/game.project @@ -1,7 +1,7 @@ [project] title = Test ; Value from fuzz.script_api as a zip -dependencies#0 = data:application/zip;base64,UEsDBBQACAAIADtMPlgAAAAAAAAAAJ8EAAAPACAAZnV6ei5zY3JpcHRfYXBpVVQNAAcCM7llAzO5ZQIzuWV1eAsAAQT1AQAABBQAAACllNFqgzAUhu/7FMeOeeFovdhdYawg29hg3UX7Aqk5tgGNkhxLJ334JWq7KNoVlitDzjnfn/w/zkCyDBcwfS2rypO7lyOh1F4upxMA+i7sES23Kdo9Rx2bfVMjcglJrqBthA92YOtYiYIgynldn2G2RaUX5hNgdiYlpiEKmgq7WkpSyphEA7arhdnxqIH2CIXKD4Ijd1GxQc3PLQVTBkEXZAcbO0gHq0kZ9e5BC94YYg8ElMMWwV4A+Rw+S012z+DAUsGhGXVRo5BKJYek2AHPPBoRFCgxKqhBjz4AHllWpNhhOu9obWKg65r+jOmk61IY7FD6ihF60lzP4++yKOk2z95Qou3UwCS07S7Ps6Pq8Jgact/sqoNheNXDk/Rt3E77pd4PPN9XYZWy9H+mOnLo8UHTkJqnbqJ+bb+vqmH1/qDhYQBBCGH4d6CGLOoQ5ldCvmvd4qNW3ZKv1nMT3aExolHWj1jkr9ab5WrTj9WdiVX9D/oBUEsHCN+ExiB1AQAAnwQAAFBLAQIUAxQACAAIADtMPljfhMYgdQEAAJ8EAAAPACAAAAAAAAAAAACkgQAAAABmdXp6LnNjcmlwdF9hcGlVVA0ABwIzuWUDM7llAjO5ZXV4CwABBPUBAAAEFAAAAFBLBQYAAAAAAQABAF0AAADSAQAAAAA= +dependencies#0 = data:application/zip;base64,UEsDBBQAAAAIAG+/Q1ieTyB9KAIAAI0HAAAPAAAAZnV6ei5zY3JpcHRfYXBpjZPFluswDIb3F97BGWbcXWYm2s9xbDXxGUfOsVXuw9+4HCUDu+ar9P8iHwuUBTwRH7qjUYLZ+wEBhsTh4wdC0LCs/qFXqYX4qSGoJ2IWYRyKjvOLNPFF9uRf5U1J4q3T0/ACihR8eBJ/C7Hw6VQJbw80zOjMI1JUVGnO6dwqikMQlIMovesZDXrdSFVGJ4uMUvpKn1aG66YnJ/O4dfV/OXA1QU6kMK0R9DLFA3U9tunGwJd61c56S+HAG8xabWcGN/cCA1mUFmqOs+zlvKUI0xiuUR/26UEGuOMlQYI9aRP9Gcsu3Wf0HwEh5gUhUcyT172SKDS9AIJAVUX3W0QA22mOSuKwGXp6qtrnSr6ym+BOvK5J/irkzRn/LGNH0t6636TpSZeHgVosn0fLlvVvj0atBe60Lf70QBycVm3dfVa769vabXM48clNt5XNN6dvXNt97my+f4NZm4yJMvzU3u78+Pvv1Y9/7Lw2UVHisB59bqKo0XWqrAyBIYeBOaUe5HUdWaDdcEV+eEW5Cczq4vKqJ72RqWWvg3yXkY60gaP586jT2Jvr1FkXNXQMgmYCzteBQf4dSKJq6IW+IZWzccjQaGLIQxpZlHvXr6NSqmuZAYPe9CQ1oCNQBJrhbmqNqrPZRbMuSBKPC90S2FCae+vnxgJDhlhnQwOWFabBAgFnaTfLuCc0lg3YLRgZlM4TZwSoA1uzQWkt24VhKzXxuRWAFDifufwHUEsBAj8AFAAAAAgAb79DWJ5PIH0oAgAAjQcAAA8AAAAAAAAAAAAgAAAAAAAAAGZ1enouc2NyaXB0X2FwaVBLBQYAAAAAAQABAD0AAABVAgAAAAA= ; defstring doesn't have a script_api dependencies#1 = https://github.com/subsoap/defstring/archive/master.zip ; The following deps all have script_api diff --git a/xtgen.mjs b/xtgen.mjs index c0daa5e..fbfd04a 100644 --- a/xtgen.mjs +++ b/xtgen.mjs @@ -13,6 +13,8 @@ const HEADER = `/** @noSelfInFile */ /// \n`; // Invalid names in TypeScript const INVALID_NAMES = [ + 'any', + 'boolean', 'break', 'case', 'catch', @@ -24,6 +26,7 @@ const INVALID_NAMES = [ 'delete', 'do', 'else', + 'enum', 'export', 'extends', 'false', @@ -31,12 +34,20 @@ const INVALID_NAMES = [ 'for', 'function', 'if', + 'implements', 'import', 'in', 'instanceof', + 'interface', + 'let', 'new', 'null', + 'package', + 'private', + 'protected', + 'public', 'return', + 'static', 'super', 'switch', 'this', @@ -44,6 +55,7 @@ const INVALID_NAMES = [ 'true', 'try', 'typeof', + 'undefined', 'var', 'void', 'while', @@ -255,15 +267,26 @@ function isApiFunc(entry) { ); } // Sanitizes name -function getName(name) { - let modifiedName = name.replace('...', 'args'); +function getName(name, isParam) { + let modifiedName = String(name); // Check against the reserved keywords in TypeScript if (INVALID_NAMES.includes(modifiedName)) { modifiedName = modifiedName + '_'; } - // Sanitize type name, allow alpha-numeric and underscore + if (isParam) { + // Special case: arguments + modifiedName = modifiedName.replace(/^\.\.\.$/, 'args'); + // Special case: Lua's `self` variable + modifiedName = modifiedName.replace(/^self$/, 'this'); + } + // Sanitize type name: allow only alpha-numeric, underscore, dollar sign modifiedName = modifiedName.replace(/[^a-zA-Z0-9_$]/g, '_'); - if (modifiedName !== name) { + // If the first character is a number, add a dollar sign to start + if (/^\d/.test(modifiedName)) { + modifiedName = '$' + modifiedName; + } + // If we're modifying a function name, not a parameter, give a warning + if (!isParam && modifiedName !== name) { console.warn(`Modifying invalid name "${name}" to "${modifiedName}"`); } return modifiedName; @@ -289,7 +312,7 @@ function getType(type, context) { return defaultType; } function sanitizeForComment(str) { - return str.replace(/\*\//g, ''); + return str.replace(/\*\//g, '*\\/'); } // Transforms and sanitizes descriptions function getComments(entry) { @@ -342,7 +365,7 @@ function getReturnComments(returnObj, newDesc) { } function getParamComments(parameters, newDesc) { parameters.forEach((param) => { - const name = param.name ? getName(param.name) : ''; + const name = param.name ? getName(param.name, true) : ''; if (name) { newDesc += `\n * @param`; if (param.type) { @@ -388,7 +411,7 @@ function getExampleComments(examples, newDesc) { // Main Functions // Function to generate TypeScript definitions for ScriptApiTable function generateTableDefinition(entry, details, start = false) { - const name = entry.name ? getName(entry.name) : DEFAULT_NAME_IF_BLANK; + const name = entry.name ? getName(entry.name, false) : DEFAULT_NAME_IF_BLANK; let tableDeclaration = `export namespace ${name} {\n`; if (start) { tableDeclaration = details.isLua @@ -411,12 +434,14 @@ function generateFunctionDefinition(entry, isParam) { return `(${parameters}) => ${returnType}`; } else { const comment = getComments(entry); - const name = entry.name ? getName(entry.name) : DEFAULT_NAME_IF_BLANK; + const name = entry.name + ? getName(entry.name, false) + : DEFAULT_NAME_IF_BLANK; return `${comment}export function ${name}(${parameters}): ${returnType};\n`; } } function getParameterDefinition(param) { - const name = param.name ? getName(param.name) : DEFAULT_NAME_IF_BLANK; + const name = param.name ? getName(param.name, true) : DEFAULT_NAME_IF_BLANK; const optional = param.optional ? '?' : ''; let type = getType(param.type, 'param'); if (type === KNOWN_TYPES['FUNCTION']) { @@ -450,7 +475,7 @@ function getReturnType(returnObj) { } // Function to generate TypeScript definitions for ScriptApiEntry function generateEntryDefinition(entry) { - const name = entry.name ? getName(entry.name) : DEFAULT_NAME_IF_BLANK; + const name = entry.name ? getName(entry.name, false) : DEFAULT_NAME_IF_BLANK; const varType = isAllUppercase(name) ? 'const' : 'let'; const type = getType(entry.type, 'return'); const comment = getComments(entry); @@ -462,7 +487,7 @@ function generateTypeScriptDefinitions(api, details) { const namespaces = {}; api.forEach((entry) => { // Handle nested properties - if (entry.name && entry.name.includes('.')) { + if (typeof entry.name === 'string' && entry.name.includes('.')) { const namePieces = entry.name.split('.'); const entryNamespace = namePieces[0]; const entryName = namePieces[1];