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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user