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 <r_ferreira@outlook.pt>

* repo-tools: changeset added

Signed-off-by: Ricardo Ferreira <r_ferreira@outlook.pt>

* repo-tools: changeset updated

Signed-off-by: Ricardo Ferreira <r_ferreira@outlook.pt>

* repo-tools: deduplicate imports on server files

Signed-off-by: Ricardo Ferreira <r_ferreira@outlook.pt>

* Update .changeset/forty-rooms-work.md

Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com>

---------

Signed-off-by: Ricardo Ferreira <r_ferreira@outlook.pt>
Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com>
Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com>
This commit is contained in:
Ricardo Ferreira
2025-10-09 16:23:40 +01:00
committed by GitHub
parent f627b49994
commit 13592794e9
5 changed files with 107 additions and 11 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': patch
---
Fixed an issue with the OpenAPI generated client and server where import/export statements were duplicated.
@@ -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'), {
@@ -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}`,
[],
@@ -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<string>();
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'));
}
@@ -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);