From 82daa27ac965b2d1f09f00d74195eb00a34ade8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:04:09 +0000 Subject: [PATCH 1/3] Initial plan From b5055e9592a5ae7638f8229002e871676ac67e9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:15:32 +0000 Subject: [PATCH 2/3] Initial investigation of the TypeScript syntax issue in 13.2.8 Co-authored-by: smorimoto <38746192+smorimoto@users.noreply.github.com> --- test-reproduction-issue.js | 105 +++++++++++++++++++++++++++++++++++++ test-reproduction.js | 93 ++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 test-reproduction-issue.js create mode 100644 test-reproduction.js diff --git a/test-reproduction-issue.js b/test-reproduction-issue.js new file mode 100644 index 00000000..fb233c4d --- /dev/null +++ b/test-reproduction-issue.js @@ -0,0 +1,105 @@ +#!/usr/bin/env node + +import { generateApi } from './dist/lib.js'; +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; + +// Test schema that should match the issue report more closely +const testSchema = { + "openapi": "3.0.0", + "info": { + "title": "Test API for issue reproduction", + "version": "1.0.0" + }, + "paths": { + "/api/leaderboard/{id}": { + "get": { + "operationId": "getLeaderboard", + "summary": "Gets a leaderboard by its ID", + "tags": ["Leaderboards"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" } + } + } + } + } + } + } + } + } + } +}; + +async function reproduce() { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-')); + const schemaPath = path.join(tmpDir, 'schema.json'); + + await fs.writeFile(schemaPath, JSON.stringify(testSchema, null, 2)); + + console.log('Generating API from schema...'); + const result = await generateApi({ + input: schemaPath, + output: tmpDir, + fileName: 'api', + silent: true, + // Try different options to see if any trigger the bug + extractRequestParams: false, + modular: false, + apiClassName: 'Leaderboards', + }); + + const generatedContent = await fs.readFile(path.join(tmpDir, 'api.ts'), 'utf8'); + + console.log('Generated content (relevant parts):'); + console.log('='.repeat(60)); + + // Find the class definition area with methods + const classMatch = generatedContent.match(/export class Leaderboards[\s\S]*$/); + if (classMatch) { + const classContent = classMatch[0]; + console.log(classContent.substring(0, 1000) + '...'); // Show first 1000 chars + } else { + console.log('Leaderboards class not found'); + // Try to find any export class + const anyClassMatch = generatedContent.match(/export class \w+[^{]*{[\s\S]*?(?=export|$)/); + if (anyClassMatch) { + console.log('Found other class:'); + console.log(anyClassMatch[0].substring(0, 500) + '...'); + } else { + console.log('No export class found'); + } + } + + console.log('='.repeat(60)); + + // Check if syntax is correct + const hasAssignment = generatedContent.includes('getLeaderboard = '); + const hasColonSyntax = generatedContent.includes('getLeaderboard: '); + + console.log(`Has assignment syntax (=): ${hasAssignment}`); + console.log(`Has colon syntax (:): ${hasColonSyntax}`); + + // Clean up + await fs.rm(tmpDir, { recursive: true }); +} + +reproduce().catch(console.error); \ No newline at end of file diff --git a/test-reproduction.js b/test-reproduction.js new file mode 100644 index 00000000..2071cb29 --- /dev/null +++ b/test-reproduction.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +import { generateApi } from './dist/lib.js'; +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; + +// Test schema similar to the issue report +const testSchema = { + "openapi": "3.0.0", + "info": { + "title": "Test API for issue reproduction", + "version": "1.0.0" + }, + "paths": { + "/": { + "get": { + "operationId": "123getUser", + "summary": "Gets a leaderboard by its ID", + + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" } + } + } + } + } + } + } + } + } + } +}; + +async function reproduce() { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-')); + const schemaPath = path.join(tmpDir, 'schema.json'); + + await fs.writeFile(schemaPath, JSON.stringify(testSchema, null, 2)); + + console.log('Generating API from schema...'); + const result = await generateApi({ + input: schemaPath, + output: tmpDir, + fileName: 'api', + silent: true + }); + + const generatedContent = await fs.readFile(path.join(tmpDir, 'api.ts'), 'utf8'); + + console.log('Generated content (relevant parts):'); + console.log('='.repeat(60)); + + // Find the class definition area with methods + const classMatch = generatedContent.match(/export class Api[\s\S]*?}$/m); + if (classMatch) { + const classContent = classMatch[0]; + console.log(classContent); + } else { + console.log('Class not found'); + } + + console.log('='.repeat(60)); + + // Check if syntax is correct + const hasAssignment = generatedContent.includes('123GetUser" = '); + const hasColonSyntax = generatedContent.includes('123GetUser": '); + + console.log(`Has assignment syntax (=): ${hasAssignment}`); + console.log(`Has colon syntax (:): ${hasColonSyntax}`); + + // Clean up + await fs.rm(tmpDir, { recursive: true }); +} + +reproduce().catch(console.error); \ No newline at end of file From f9ab68df0bb22d8aae495b3cd3762d84b37a4039 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:24:02 +0000 Subject: [PATCH 3/3] Fix TypeScript syntax issue for operationIds starting with numbers Co-authored-by: smorimoto <38746192+smorimoto@users.noreply.github.com> --- templates/default/api.ejs | 4 +- templates/default/procedure-call.ejs | 4 +- templates/modular/api.ejs | 2 +- templates/modular/procedure-call.ejs | 4 +- test-reproduction-issue.js | 105 ---------- test-reproduction.js | 93 --------- .../spec/typescript-syntax-fix/basic.test.ts | 186 ++++++++++++++++++ 7 files changed, 193 insertions(+), 205 deletions(-) delete mode 100644 test-reproduction-issue.js delete mode 100644 test-reproduction.js create mode 100644 tests/spec/typescript-syntax-fix/basic.test.ts diff --git a/templates/default/api.ejs b/templates/default/api.ejs index a393c917..b631ba89 100644 --- a/templates/default/api.ejs +++ b/templates/default/api.ejs @@ -50,7 +50,7 @@ export class <%~ config.apiClassName %><% if ( <% if (routes.outOfModule) { %> <% for (const route of routes.outOfModule) { %> - <%~ includeFile('./procedure-call.ejs', { ...it, route }) %> + <%~ includeFile('./procedure-call.ejs', { ...it, route, isObjectProperty: false }) %> <% } %> <% } %> @@ -60,7 +60,7 @@ export class <%~ config.apiClassName %><% if ( <%~ moduleName %> = { <% for (const route of combinedRoutes) { %> - <%~ includeFile('./procedure-call.ejs', { ...it, route }) %> + <%~ includeFile('./procedure-call.ejs', { ...it, route, isObjectProperty: true }) %> <% } %> } diff --git a/templates/default/procedure-call.ejs b/templates/default/procedure-call.ejs index 90af47bd..d197037e 100644 --- a/templates/default/procedure-call.ejs +++ b/templates/default/procedure-call.ejs @@ -1,5 +1,5 @@ <% -const { utils, route, config } = it; +const { utils, route, config, isObjectProperty = route.namespace } = it; const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route; const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils; const { parameters, path, method, payload, query, formData, security, requestParams } = route.request; @@ -90,7 +90,7 @@ const isValidIdentifier = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name); <%~ routeDocs.lines %> */ -<% if (isValidIdentifier(route.routeName.usage)) { %><%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %><% } else { %>"<%~ route.routeName.usage %>"<%~ route.namespace ? ': ' : ' = ' %><% } %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> => +<% if (isValidIdentifier(route.routeName.usage)) { %><%~ route.routeName.usage %><%~ isObjectProperty ? ': ' : ' = ' %><% } else { %>"<%~ route.routeName.usage %>"<%~ isObjectProperty ? ': ' : ' = ' %><% } %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> => <%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({ path: `<%~ path %>`, method: '<%~ _.upperCase(method) %>', diff --git a/templates/modular/api.ejs b/templates/modular/api.ejs index 25bfa182..070f38b3 100644 --- a/templates/modular/api.ejs +++ b/templates/modular/api.ejs @@ -23,6 +23,6 @@ export class <%= apiClassName %><% if (!config.singl <% } %> <% for (const route of routes) { %> - <%~ includeFile('./procedure-call.ejs', { ...it, route }) %> + <%~ includeFile('./procedure-call.ejs', { ...it, route, isObjectProperty: false }) %> <% } %> } diff --git a/templates/modular/procedure-call.ejs b/templates/modular/procedure-call.ejs index 90af47bd..d197037e 100644 --- a/templates/modular/procedure-call.ejs +++ b/templates/modular/procedure-call.ejs @@ -1,5 +1,5 @@ <% -const { utils, route, config } = it; +const { utils, route, config, isObjectProperty = route.namespace } = it; const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route; const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils; const { parameters, path, method, payload, query, formData, security, requestParams } = route.request; @@ -90,7 +90,7 @@ const isValidIdentifier = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name); <%~ routeDocs.lines %> */ -<% if (isValidIdentifier(route.routeName.usage)) { %><%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %><% } else { %>"<%~ route.routeName.usage %>"<%~ route.namespace ? ': ' : ' = ' %><% } %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> => +<% if (isValidIdentifier(route.routeName.usage)) { %><%~ route.routeName.usage %><%~ isObjectProperty ? ': ' : ' = ' %><% } else { %>"<%~ route.routeName.usage %>"<%~ isObjectProperty ? ': ' : ' = ' %><% } %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> => <%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({ path: `<%~ path %>`, method: '<%~ _.upperCase(method) %>', diff --git a/test-reproduction-issue.js b/test-reproduction-issue.js deleted file mode 100644 index fb233c4d..00000000 --- a/test-reproduction-issue.js +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env node - -import { generateApi } from './dist/lib.js'; -import fs from 'fs/promises'; -import path from 'path'; -import os from 'os'; - -// Test schema that should match the issue report more closely -const testSchema = { - "openapi": "3.0.0", - "info": { - "title": "Test API for issue reproduction", - "version": "1.0.0" - }, - "paths": { - "/api/leaderboard/{id}": { - "get": { - "operationId": "getLeaderboard", - "summary": "Gets a leaderboard by its ID", - "tags": ["Leaderboards"], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { "type": "integer" }, - "name": { "type": "string" } - } - } - } - } - } - } - } - } - } -}; - -async function reproduce() { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-')); - const schemaPath = path.join(tmpDir, 'schema.json'); - - await fs.writeFile(schemaPath, JSON.stringify(testSchema, null, 2)); - - console.log('Generating API from schema...'); - const result = await generateApi({ - input: schemaPath, - output: tmpDir, - fileName: 'api', - silent: true, - // Try different options to see if any trigger the bug - extractRequestParams: false, - modular: false, - apiClassName: 'Leaderboards', - }); - - const generatedContent = await fs.readFile(path.join(tmpDir, 'api.ts'), 'utf8'); - - console.log('Generated content (relevant parts):'); - console.log('='.repeat(60)); - - // Find the class definition area with methods - const classMatch = generatedContent.match(/export class Leaderboards[\s\S]*$/); - if (classMatch) { - const classContent = classMatch[0]; - console.log(classContent.substring(0, 1000) + '...'); // Show first 1000 chars - } else { - console.log('Leaderboards class not found'); - // Try to find any export class - const anyClassMatch = generatedContent.match(/export class \w+[^{]*{[\s\S]*?(?=export|$)/); - if (anyClassMatch) { - console.log('Found other class:'); - console.log(anyClassMatch[0].substring(0, 500) + '...'); - } else { - console.log('No export class found'); - } - } - - console.log('='.repeat(60)); - - // Check if syntax is correct - const hasAssignment = generatedContent.includes('getLeaderboard = '); - const hasColonSyntax = generatedContent.includes('getLeaderboard: '); - - console.log(`Has assignment syntax (=): ${hasAssignment}`); - console.log(`Has colon syntax (:): ${hasColonSyntax}`); - - // Clean up - await fs.rm(tmpDir, { recursive: true }); -} - -reproduce().catch(console.error); \ No newline at end of file diff --git a/test-reproduction.js b/test-reproduction.js deleted file mode 100644 index 2071cb29..00000000 --- a/test-reproduction.js +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env node - -import { generateApi } from './dist/lib.js'; -import fs from 'fs/promises'; -import path from 'path'; -import os from 'os'; - -// Test schema similar to the issue report -const testSchema = { - "openapi": "3.0.0", - "info": { - "title": "Test API for issue reproduction", - "version": "1.0.0" - }, - "paths": { - "/": { - "get": { - "operationId": "123getUser", - "summary": "Gets a leaderboard by its ID", - - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { "type": "integer" }, - "name": { "type": "string" } - } - } - } - } - } - } - } - } - } -}; - -async function reproduce() { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-')); - const schemaPath = path.join(tmpDir, 'schema.json'); - - await fs.writeFile(schemaPath, JSON.stringify(testSchema, null, 2)); - - console.log('Generating API from schema...'); - const result = await generateApi({ - input: schemaPath, - output: tmpDir, - fileName: 'api', - silent: true - }); - - const generatedContent = await fs.readFile(path.join(tmpDir, 'api.ts'), 'utf8'); - - console.log('Generated content (relevant parts):'); - console.log('='.repeat(60)); - - // Find the class definition area with methods - const classMatch = generatedContent.match(/export class Api[\s\S]*?}$/m); - if (classMatch) { - const classContent = classMatch[0]; - console.log(classContent); - } else { - console.log('Class not found'); - } - - console.log('='.repeat(60)); - - // Check if syntax is correct - const hasAssignment = generatedContent.includes('123GetUser" = '); - const hasColonSyntax = generatedContent.includes('123GetUser": '); - - console.log(`Has assignment syntax (=): ${hasAssignment}`); - console.log(`Has colon syntax (:): ${hasColonSyntax}`); - - // Clean up - await fs.rm(tmpDir, { recursive: true }); -} - -reproduce().catch(console.error); \ No newline at end of file diff --git a/tests/spec/typescript-syntax-fix/basic.test.ts b/tests/spec/typescript-syntax-fix/basic.test.ts new file mode 100644 index 00000000..cc2c0a5f --- /dev/null +++ b/tests/spec/typescript-syntax-fix/basic.test.ts @@ -0,0 +1,186 @@ +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { generateApi } from "../../../src/index.js"; + +describe("TypeScript syntax fix for operationIds starting with numbers", async () => { + let tmpdir: string; + + beforeAll(async () => { + tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); + }); + + afterAll(async () => { + await fs.rm(tmpdir, { recursive: true }); + }); + + test("should use assignment syntax for direct class methods", async () => { + const schema = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/": { + "get": { + "operationId": "123getUser", + "summary": "Get user with operationId starting with number", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" } + } + } + } + } + } + } + } + } + } + }; + + const schemaPath = path.join(tmpdir, "direct-method-schema.json"); + await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2)); + + await generateApi({ + fileName: "direct-method", + input: schemaPath, + output: tmpdir, + silent: true, + generateClient: true, + }); + + const content = await fs.readFile(path.join(tmpdir, "direct-method.ts"), { + encoding: "utf8", + }); + + // Should use assignment syntax for direct class methods + expect(content).toContain('"123GetUser" = '); + // Should not use colon syntax for direct class methods + expect(content).not.toContain('"123GetUser": '); + }); + + test("should use colon syntax for object properties", async () => { + const schema = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/api/user/{id}": { + "get": { + "operationId": "456getUser", + "summary": "Get user with operationId starting with number", + "tags": ["Users"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" } + } + } + } + } + } + } + } + } + } + }; + + const schemaPath = path.join(tmpdir, "object-property-schema.json"); + await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2)); + + await generateApi({ + fileName: "object-property", + input: schemaPath, + output: tmpdir, + silent: true, + generateClient: true, + }); + + const content = await fs.readFile(path.join(tmpdir, "object-property.ts"), { + encoding: "utf8", + }); + + // Should use colon syntax for object properties + expect(content).toContain('"456GetUser": '); + // Should not use assignment syntax for object properties + expect(content).not.toContain('"456GetUser" = '); + }); + + test("should handle normal identifiers correctly", async () => { + const schema = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/": { + "get": { + "operationId": "normalMethod", + "summary": "Normal method", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + } + } + }; + + const schemaPath = path.join(tmpdir, "normal-method-schema.json"); + await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2)); + + await generateApi({ + fileName: "normal-method", + input: schemaPath, + output: tmpdir, + silent: true, + generateClient: true, + }); + + const content = await fs.readFile(path.join(tmpdir, "normal-method.ts"), { + encoding: "utf8", + }); + + // Should use assignment syntax for direct class methods (no quotes needed) + expect(content).toContain('normalMethod = '); + // Should not use colon syntax for direct class methods + expect(content).not.toContain('normalMethod: '); + }); +}); \ No newline at end of file