mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
265 lines
8.9 KiB
TypeScript
265 lines
8.9 KiB
TypeScript
const packageRegex =
|
|
/import(?:(?:(?:[ \n\t]+([^ *\n\t\{\},]+)[ \n\t]*(?:,|[ \n\t]+))?([ \n\t]*\{(?:[ \n\t]*[^ \n\t"'\{\}]+[ \n\t]*,?)+\})?[ \n\t]*)|[ \n\t]*\*[ \n\t]*as[ \n\t]+([^ \n\t\{\}]+)[ \n\t]+)from[ \n\t]*(?:['"])([^'"\n]+)(?:['"])(?:;?)/g;
|
|
|
|
const nameChangeRegex = /((?:\w|\s|_)*)( as )((?:\w|\s|_)*)( |,)?/g;
|
|
|
|
export type ImportMatch = {
|
|
statement: string;
|
|
importPath: string;
|
|
defaultImport?: string;
|
|
namedImports?: string;
|
|
namespaceImport?: string;
|
|
};
|
|
|
|
export type NameChangeMatch = {
|
|
statement: string;
|
|
fromName: string;
|
|
toName: string;
|
|
afterCharacter?: string;
|
|
};
|
|
|
|
export const getImports = (content: string): Array<ImportMatch> => {
|
|
const matches = content.matchAll(packageRegex);
|
|
|
|
const imports: Array<ImportMatch> = [];
|
|
|
|
for (const match of matches) {
|
|
const [
|
|
statement,
|
|
defaultImport,
|
|
namedImports,
|
|
namespaceImport,
|
|
importPath,
|
|
] = match;
|
|
|
|
imports.push({
|
|
statement,
|
|
importPath,
|
|
...(defaultImport && { defaultImport }),
|
|
...(namedImports && { namedImports }),
|
|
...(namespaceImport && { namespaceImport }),
|
|
});
|
|
}
|
|
|
|
return imports?.filter(Boolean);
|
|
};
|
|
|
|
export const getNameChangeInImport = (
|
|
namedImportString: string,
|
|
): Array<NameChangeMatch> => {
|
|
const matches = namedImportString.matchAll(nameChangeRegex);
|
|
|
|
const nameChanges: Array<NameChangeMatch> = [];
|
|
|
|
for (const match of matches) {
|
|
const [statement, fromName, _as, toName, afterCharacter] = match;
|
|
|
|
nameChanges.push({
|
|
statement,
|
|
fromName: fromName.trim(),
|
|
toName: toName.trim(),
|
|
afterCharacter,
|
|
});
|
|
}
|
|
|
|
return nameChanges;
|
|
};
|
|
|
|
/** @internal */
|
|
export const getContentBeforeImport = (
|
|
content: string,
|
|
importMatch: ImportMatch,
|
|
): string => {
|
|
// get the content before the import statement and between the last import statement and the current one
|
|
const contentBeforeImport = content.substring(
|
|
0,
|
|
content.indexOf(importMatch.statement),
|
|
);
|
|
// get the last import statement
|
|
const lastImportStatement = getImports(contentBeforeImport).pop();
|
|
|
|
// if there is no last import statement, return the content before the current import statement
|
|
if (!lastImportStatement) {
|
|
return contentBeforeImport;
|
|
}
|
|
|
|
// get the content between the last import statement and the current one
|
|
const contentBetweenImports = contentBeforeImport.substring(
|
|
contentBeforeImport.indexOf(lastImportStatement?.statement) +
|
|
lastImportStatement?.statement?.length,
|
|
);
|
|
|
|
// return the content before the current import statement and between the last import statement and the current one
|
|
return contentBetweenImports;
|
|
};
|
|
|
|
/** @internal */
|
|
export const isImportHasBeforeContent = (
|
|
content: string,
|
|
importMatch: ImportMatch,
|
|
): boolean => {
|
|
const contentBeforeImport = importMatch
|
|
? getContentBeforeImport(content, importMatch)
|
|
: "";
|
|
|
|
return !!contentBeforeImport.trim();
|
|
};
|
|
|
|
const IMPORT_ORDER = ["react", "@refinedev/core", "@refinedev/"];
|
|
|
|
export const reorderImports = (content: string): string => {
|
|
let newContent = content;
|
|
// imports can have comments before them, we need to preserve those comments and import statements.
|
|
// so we need to filter out the imports with comments before.
|
|
const allImports = getImports(content);
|
|
// remove `import type` imports
|
|
const allModuleImports = allImports.filter(
|
|
(importMatch) => !importMatch.statement.includes("import type "),
|
|
);
|
|
const typeImports = allImports.filter((importMatch) =>
|
|
importMatch.statement.includes("import type"),
|
|
);
|
|
|
|
const importsWithBeforeContent: ImportMatch[] = [];
|
|
const importsWithoutBeforeContent: ImportMatch[] = [];
|
|
|
|
allModuleImports.forEach((importMatch) => {
|
|
if (isImportHasBeforeContent(content, importMatch)) {
|
|
importsWithBeforeContent.push(importMatch);
|
|
} else {
|
|
importsWithoutBeforeContent.push(importMatch);
|
|
}
|
|
});
|
|
|
|
// insertion point is the first import statement, others will be replaced to empty string and added to the first import line
|
|
const insertionPoint = newContent.indexOf(
|
|
importsWithoutBeforeContent?.[0]?.statement,
|
|
);
|
|
|
|
// remove all the imports without comments before
|
|
importsWithoutBeforeContent.forEach((importMatch) => {
|
|
newContent = newContent.replace(importMatch.statement, "");
|
|
});
|
|
|
|
// remove all type imports
|
|
typeImports.forEach((importMatch) => {
|
|
newContent = newContent.replace(importMatch.statement, "");
|
|
});
|
|
|
|
// we need to merge the imports from the same package unless one of them is a namespace import]
|
|
const importsByPackage = importsWithoutBeforeContent.reduce(
|
|
(acc, importMatch) => {
|
|
const { importPath } = importMatch;
|
|
|
|
if (acc[importPath]) {
|
|
acc[importPath].push(importMatch);
|
|
} else {
|
|
acc[importPath] = [importMatch];
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
{} as Record<string, ImportMatch[]>,
|
|
);
|
|
|
|
// merge the imports from the same package
|
|
const mergedImports = Object.entries(importsByPackage).map(
|
|
([importPath, importMatches]) => {
|
|
// example: A
|
|
const defaultImport = importMatches.find(
|
|
(importMatch) => importMatch.defaultImport,
|
|
);
|
|
|
|
// example: * as A
|
|
const namespaceImport = importMatches.find(
|
|
(importMatch) => importMatch.namespaceImport,
|
|
);
|
|
|
|
// example: { A, B }
|
|
// example: { A as C, B }
|
|
// content inside the curly braces should be merged
|
|
const namedImports = importMatches
|
|
.filter((importMatch) => importMatch.namedImports)
|
|
.map((importMatch) => {
|
|
// remove curly braces and trim then split by comma (can be multiline)
|
|
const namedImports = (importMatch.namedImports ?? "")
|
|
.replace(/{|}/g, "")
|
|
.trim()
|
|
.split(",")
|
|
.map((namedImport) => namedImport.trim());
|
|
|
|
return namedImports.filter(Boolean).join(", ");
|
|
})
|
|
.join(", ");
|
|
|
|
let importLine = "";
|
|
|
|
// default import and namespace import can not be used together
|
|
// but we can use default import and named imports together
|
|
// so we need to merge them
|
|
if (namespaceImport) {
|
|
importLine += namespaceImport.statement + "\n";
|
|
}
|
|
if (defaultImport || namedImports) {
|
|
if (defaultImport && namedImports) {
|
|
importLine += `import ${defaultImport.defaultImport}, { ${namedImports} } from "${importMatches[0].importPath}";\n`;
|
|
} else if (defaultImport) {
|
|
importLine += `import ${defaultImport.defaultImport} from "${importMatches[0].importPath}";\n`;
|
|
} else {
|
|
importLine += `import { ${namedImports} } from "${importMatches[0].importPath}";\n`;
|
|
}
|
|
}
|
|
|
|
return [importPath, importLine] as [
|
|
importPath: string,
|
|
importLine: string,
|
|
];
|
|
},
|
|
);
|
|
|
|
// sort the imports without comments before
|
|
// sort should be done by IMPORT_ORDER and alphabetically
|
|
// priority is exact match in IMPORT_ORDER, then includes match in IMPORT_ORDER, then alphabetically
|
|
const sortedImports = [...mergedImports].sort(
|
|
([aImportPath], [bImportPath]) => {
|
|
const aImportOrderIndex = IMPORT_ORDER.findIndex((order) =>
|
|
aImportPath.includes(order),
|
|
);
|
|
const bImportOrderIndex = IMPORT_ORDER.findIndex((order) =>
|
|
bImportPath.includes(order),
|
|
);
|
|
|
|
if (aImportOrderIndex === bImportOrderIndex) {
|
|
return aImportPath.localeCompare(bImportPath);
|
|
}
|
|
|
|
if (aImportOrderIndex === -1) {
|
|
return 1;
|
|
}
|
|
|
|
if (bImportOrderIndex === -1) {
|
|
return -1;
|
|
}
|
|
|
|
return aImportOrderIndex - bImportOrderIndex;
|
|
},
|
|
);
|
|
|
|
// add the sorted imports to the insertion point keep the before and after content
|
|
// add the type imports after the sorted imports
|
|
const joinedModuleImports = sortedImports
|
|
.map(([, importLine]) => importLine)
|
|
.join("");
|
|
const joinedTypeImports = typeImports
|
|
.map((importMatch) => importMatch.statement)
|
|
.join("\n");
|
|
|
|
newContent =
|
|
newContent.substring(0, insertionPoint) +
|
|
joinedModuleImports +
|
|
joinedTypeImports +
|
|
newContent.substring(insertionPoint);
|
|
|
|
return newContent;
|
|
};
|