From 13592794e915bfda19220cba186b48050721c98e Mon Sep 17 00:00:00 2001 From: Ricardo Ferreira Date: Thu, 9 Oct 2025 16:23:40 +0100 Subject: [PATCH] repo-tools: Fix duplicate imports in OpenAPI client generation (#31297) * repo-tools: Fix duplicate imports in OpenAPI client generation Added automatic deduplication step to remove duplicate import/export statements from generated TypeScript clients while preserving code structure and different import syntaxes. Signed-off-by: Ricardo Ferreira * repo-tools: changeset added Signed-off-by: Ricardo Ferreira * repo-tools: changeset updated Signed-off-by: Ricardo Ferreira * repo-tools: deduplicate imports on server files Signed-off-by: Ricardo Ferreira * Update .changeset/forty-rooms-work.md Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> --------- Signed-off-by: Ricardo Ferreira Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> --- .changeset/forty-rooms-work.md | 5 ++ .../package/schema/openapi/generate/client.ts | 9 +++ .../package/schema/openapi/generate/server.ts | 19 +++-- .../src/lib/openapi/dedupe-imports.ts | 74 +++++++++++++++++++ .../repo-tools/src/lib/openapi/helpers.ts | 11 ++- 5 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 .changeset/forty-rooms-work.md create mode 100644 packages/repo-tools/src/lib/openapi/dedupe-imports.ts diff --git a/.changeset/forty-rooms-work.md b/.changeset/forty-rooms-work.md new file mode 100644 index 0000000000..e67016ec44 --- /dev/null +++ b/.changeset/forty-rooms-work.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Fixed an issue with the OpenAPI generated client and server where import/export statements were duplicated. diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts index edcb49a9be..8a8817c721 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts @@ -28,6 +28,7 @@ import { getPathToCurrentOpenApiSpec, toGeneratorAdditionalProperties, } from '../../../../../lib/openapi/helpers'; +import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports'; async function generate( outputDirectory: string, @@ -101,6 +102,14 @@ async function generate( }); } + // Deduplicate imports in generated files + const generatedFiles = await fs.readdir(resolvedOutputDirectory); + for (const file of generatedFiles) { + if (file.endsWith('.ts')) { + deduplicateImports(resolve(resolvedOutputDirectory, file)); + } + } + fs.removeSync(resolve(resolvedOutputDirectory, '.openapi-generator-ignore')); fs.rmSync(resolve(resolvedOutputDirectory, '.openapi-generator'), { diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts index c51b63cf54..130c93cf71 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts @@ -14,24 +14,25 @@ * limitations under the License. */ +import { resolvePackagePath } from '@backstage/backend-plugin-api'; import chalk from 'chalk'; -import { resolve, dirname, join } from 'path'; +import fs from 'fs-extra'; import YAML from 'js-yaml'; +import { dirname, join, resolve } from 'path'; +import { exec } from '../../../../../lib/exec'; import { OLD_SCHEMA_PATH, OPENAPI_IGNORE_FILES, OUTPUT_PATH, TS_SCHEMA_PATH, } from '../../../../../lib/openapi/constants'; -import { paths as cliPaths } from '../../../../../lib/paths'; -import fs from 'fs-extra'; -import { exec } from '../../../../../lib/exec'; -import { resolvePackagePath } from '@backstage/backend-plugin-api'; +import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports'; import { getPathToCurrentOpenApiSpec, getRelativePathToFile, toGeneratorAdditionalProperties, } from '../../../../../lib/openapi/helpers'; +import { paths as cliPaths } from '../../../../../lib/paths'; async function generateSpecFile() { const openapiPath = await getPathToCurrentOpenApiSpec(); @@ -133,6 +134,14 @@ async function generate( }, ); + // Deduplicate imports in generated files + const generatedFiles = await fs.readdir(resolvedOutputDirectory); + for (const file of generatedFiles) { + if (file.endsWith('.ts')) { + deduplicateImports(resolve(resolvedOutputDirectory, file)); + } + } + await exec( `yarn backstage-cli package lint --fix ${resolvedOutputDirectory}`, [], diff --git a/packages/repo-tools/src/lib/openapi/dedupe-imports.ts b/packages/repo-tools/src/lib/openapi/dedupe-imports.ts new file mode 100644 index 0000000000..c398a43e82 --- /dev/null +++ b/packages/repo-tools/src/lib/openapi/dedupe-imports.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; + +/** + * Removes duplicate import and export statements from TypeScript/JavaScript files. + * Preserves different import syntaxes, multi-line imports, and code structure. + * + * @param filePath - Absolute path to the file to deduplicate + */ +export function deduplicateImports(filePath: string): void { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + const seen = new Set(); + const result: string[] = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + const trimmed = line.trim(); + + // Handle import/export statements + if (trimmed.startsWith('import ') || trimmed.startsWith('export ')) { + let fullStatement = line; + let endIndex = i; + + // Check if multi-line (has { but no }) + if (trimmed.includes('{') && !trimmed.includes('}')) { + // Collect full multi-line statement + while (endIndex < lines.length && !lines[endIndex].includes('}')) { + endIndex++; + } + if (endIndex < lines.length) { + fullStatement = lines.slice(i, endIndex + 1).join('\n'); + } + } + + // Normalize for comparison (remove comments, extra spaces) + const normalized = fullStatement + .replace(/\/\/.*$/gm, '') + .replace(/\s+/g, ' ') + .trim(); + + if (!seen.has(normalized)) { + seen.add(normalized); + // Add all lines of the statement + for (let j = i; j <= endIndex; j++) { + result.push(lines[j]); + } + } + + i = endIndex + 1; + } else { + result.push(line); + i++; + } + } + + fs.writeFileSync(filePath, result.join('\n')); +} diff --git a/packages/repo-tools/src/lib/openapi/helpers.ts b/packages/repo-tools/src/lib/openapi/helpers.ts index 868623b874..0eaa090ac4 100644 --- a/packages/repo-tools/src/lib/openapi/helpers.ts +++ b/packages/repo-tools/src/lib/openapi/helpers.ts @@ -14,14 +14,13 @@ * limitations under the License. */ -import { pathExists } from 'fs-extra'; -import { paths } from '../paths'; -import { YAML_SCHEMA_PATH } from './constants'; -import { resolve } from 'path'; +import Parser from '@apidevtools/swagger-parser'; +import fs, { pathExists } from 'fs-extra'; import YAML from 'js-yaml'; import { cloneDeep } from 'lodash'; -import Parser from '@apidevtools/swagger-parser'; -import fs from 'fs-extra'; +import { resolve } from 'path'; +import { paths } from '../paths'; +import { YAML_SCHEMA_PATH } from './constants'; export const getPathToFile = async (directory: string, filename: string) => { return resolve(directory, filename);