mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
merge conflicts resolved.
This commit is contained in:
227
scripts/fix-duplicate-imports.mjs
Normal file
227
scripts/fix-duplicate-imports.mjs
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* fix-duplicate-imports.mjs
|
||||
*
|
||||
* Merges duplicate import statements from the same module across all TypeScript
|
||||
* source files in the project. Run this whenever `npm run lint` reports
|
||||
* `no-duplicate-imports` errors.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/fix-duplicate-imports.mjs
|
||||
*
|
||||
* Handles:
|
||||
* import type { A, B } from 'module' — type-only imports
|
||||
* import Default from 'module' — default imports
|
||||
* import Default, { A, B } from 'mod' — mixed default + named
|
||||
* import { A, B } from 'module' — named imports
|
||||
* import {\n A,\n B\n} from 'mod' — multi-line named imports
|
||||
*
|
||||
* When merging a `import type` with a value import from the same module,
|
||||
* type-only specifiers are inlined as `type Name` in the merged statement.
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
||||
import { extname, join, resolve } from "path";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const projectRoot = resolve(new URL(".", import.meta.url).pathname, "..");
|
||||
const srcDir = join(projectRoot, "src");
|
||||
const extensions = new Set([".ts", ".tsx"]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File discovery — recursively find all TS/TSX files under src/
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function findFiles(dir) {
|
||||
const results = [];
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = join(dir, entry);
|
||||
if (statSync(full).isDirectory()) {
|
||||
results.push(...findFiles(full));
|
||||
} else if (extensions.has(extname(entry))) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Import extraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Extracts all top-level import statements from file content.
|
||||
* Returns array of { start, end, raw, module, isType, defaultImport, namedImports }
|
||||
* where start/end are character positions in the content.
|
||||
*/
|
||||
function extractImports(content) {
|
||||
const results = [];
|
||||
const importRe = /^import\s+([\s\S]*?)from\s+['"]([^'"]+)['"]\s*;?/gm;
|
||||
|
||||
let match;
|
||||
while ((match = importRe.exec(content)) !== null) {
|
||||
const raw = match[0];
|
||||
const specifierPart = match[1];
|
||||
const moduleName = match[2];
|
||||
|
||||
const isType = /^type\s+/.test(specifierPart.trimStart());
|
||||
const cleanSpec = specifierPart.replace(/^type\s+/, "").trim();
|
||||
|
||||
let defaultImport = null;
|
||||
let namedStr = null;
|
||||
|
||||
const namespaceMatch = cleanSpec.match(/^\*\s+as\s+(\w+)/);
|
||||
if (namespaceMatch) {
|
||||
defaultImport = `* as ${namespaceMatch[1]}`;
|
||||
} else {
|
||||
const braceIdx = cleanSpec.indexOf("{");
|
||||
if (braceIdx === -1) {
|
||||
const def = cleanSpec.replace(/,$/, "").trim();
|
||||
if (def) defaultImport = def;
|
||||
} else {
|
||||
const beforeBrace = cleanSpec.slice(0, braceIdx).replace(/,$/, "").trim();
|
||||
if (beforeBrace) defaultImport = beforeBrace;
|
||||
const closeBrace = cleanSpec.lastIndexOf("}");
|
||||
namedStr = cleanSpec.slice(braceIdx + 1, closeBrace);
|
||||
}
|
||||
}
|
||||
|
||||
const namedImports = namedStr
|
||||
? namedStr.split(",").map((s) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
results.push({
|
||||
start: match.index,
|
||||
end: match.index + raw.length,
|
||||
raw,
|
||||
module: moduleName,
|
||||
isType,
|
||||
defaultImport,
|
||||
namedImports,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Import merging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a single merged import statement from multiple imports of the same module.
|
||||
*
|
||||
* - All type imports → merged `import type { ... }`
|
||||
* - Mixed type + value → merged value import with inline `type Name` specifiers
|
||||
*/
|
||||
function buildMergedImport(moduleName, importList) {
|
||||
const allType = importList.every((i) => i.isType);
|
||||
|
||||
if (allType) {
|
||||
const allNamed = new Set(importList.flatMap((i) => i.namedImports));
|
||||
const defaultImport = importList.map((i) => i.defaultImport).find(Boolean) ?? null;
|
||||
const parts = [];
|
||||
if (defaultImport) parts.push(defaultImport);
|
||||
if (allNamed.size > 0) parts.push(`{ ${[...allNamed].join(", ")} }`);
|
||||
if (parts.length === 0) return `import type "${moduleName}";`;
|
||||
return `import type ${parts.join(", ")} from "${moduleName}";`;
|
||||
}
|
||||
|
||||
// Mixed: collect value default, type-only named, value named separately
|
||||
let valueDefault = null;
|
||||
const typeNamed = new Set();
|
||||
const valueNamed = new Set();
|
||||
|
||||
for (const imp of importList) {
|
||||
if (imp.defaultImport && !imp.isType) valueDefault = imp.defaultImport;
|
||||
for (const n of imp.namedImports) {
|
||||
(imp.isType ? typeNamed : valueNamed).add(n);
|
||||
}
|
||||
}
|
||||
|
||||
// Build named specifiers: `type X` for type-only, plain for value
|
||||
const typeSpecifiers = [...typeNamed].filter((n) => !valueNamed.has(n)).map((n) => `type ${n}`);
|
||||
const valueSpecifiers = [...valueNamed];
|
||||
const namedParts = [...typeSpecifiers, ...valueSpecifiers];
|
||||
|
||||
const parts = [];
|
||||
if (valueDefault) parts.push(valueDefault);
|
||||
if (namedParts.length > 0) parts.push(`{ ${namedParts.join(", ")} }`);
|
||||
if (parts.length === 0) return `import "${moduleName}";`;
|
||||
return `import ${parts.join(", ")} from "${moduleName}";`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File fixer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function fixFile(filePath) {
|
||||
let content = readFileSync(filePath, "utf-8");
|
||||
const imports = extractImports(content);
|
||||
if (imports.length === 0) return null;
|
||||
|
||||
// Group by module
|
||||
const byModule = new Map();
|
||||
for (const imp of imports) {
|
||||
if (!byModule.has(imp.module)) byModule.set(imp.module, []);
|
||||
byModule.get(imp.module).push(imp);
|
||||
}
|
||||
|
||||
if (![...byModule.values()].some((v) => v.length > 1)) return null;
|
||||
|
||||
// Build merged text for each module
|
||||
const mergedMap = new Map();
|
||||
for (const [mod, imps] of byModule) {
|
||||
mergedMap.set(mod, imps.length === 1 ? imps[0].raw : buildMergedImport(mod, imps));
|
||||
}
|
||||
|
||||
// Build replacement list (process in reverse order to preserve character positions)
|
||||
imports.sort((a, b) => a.start - b.start);
|
||||
const placedModules = new Set();
|
||||
const replacements = [];
|
||||
|
||||
for (const imp of imports) {
|
||||
if (!placedModules.has(imp.module)) {
|
||||
replacements.push({ start: imp.start, end: imp.end, replacement: mergedMap.get(imp.module) });
|
||||
placedModules.add(imp.module);
|
||||
} else {
|
||||
// Remove duplicate, including its trailing newline
|
||||
const end = content[imp.end] === "\n" ? imp.end + 1 : imp.end;
|
||||
replacements.push({ start: imp.start, end, replacement: "" });
|
||||
}
|
||||
}
|
||||
|
||||
replacements.sort((a, b) => b.start - a.start);
|
||||
for (const { start, end, replacement } of replacements) {
|
||||
content = content.slice(0, start) + replacement + content.slice(end);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const files = findFiles(srcDir);
|
||||
let fixedCount = 0;
|
||||
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const original = readFileSync(filePath, "utf-8");
|
||||
const fixed = fixFile(filePath);
|
||||
if (fixed && fixed !== original) {
|
||||
writeFileSync(filePath, fixed, "utf-8");
|
||||
const rel = filePath.replace(projectRoot + "/", "");
|
||||
console.log(`Fixed: ${rel}`);
|
||||
fixedCount++;
|
||||
}
|
||||
} catch (err) {
|
||||
const rel = filePath.replace(projectRoot + "/", "");
|
||||
console.error(`Error: ${rel} — ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nDone. Fixed ${fixedCount} file${fixedCount !== 1 ? "s" : ""}.`);
|
||||
Reference in New Issue
Block a user