This commit is contained in:
Stefan Pejcic
2024-11-07 19:03:37 +01:00
parent c6df945ed5
commit 09f9f9502d
2472 changed files with 620417 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import { appendAfterImports } from "./appendAfterImports";
describe("appendAfterImports", () => {
it("should append after imports", () => {
const content = `
import { foo } from "bar";
import { bar } from "foo";
import { baz } from "baz";
`;
const append = `console.log("hello world");`;
const result = appendAfterImports(content, append);
expect(result).toEqual(`
import { foo } from "bar";
import { bar } from "foo";
import { baz } from "baz";
console.log("hello world");
`);
});
});

View File

@@ -0,0 +1,17 @@
import { getImports } from "./import";
export const appendAfterImports = (content: string, append: string): string => {
const imports = getImports(content);
const lastImport = imports[imports.length - 1];
const lastImportIndex = lastImport
? content.indexOf(lastImport.statement)
: content.length - 1;
return `${content.slice(
0,
lastImportIndex + lastImport?.statement.length,
)}\n${append}\n${content.slice(
lastImportIndex + lastImport?.statement.length,
)}`;
};

View File

@@ -0,0 +1,8 @@
export const SWIZZLE_CODES = {
SUCCESS: "SUCCESS",
UNKNOWN_ERROR: "UNKNOWN_ERROR",
SOURCE_PATH_NOT_FOUND: "SOURCE_PATH_NOT_FOUND",
TARGET_PATH_NOT_FOUND: "TARGET_PATH_NOT_FOUND",
SOURCE_PATH_NOT_A_FILE: "SOURCE_PATH_NOT_A_FILE",
TARGET_ALREADY_EXISTS: "TARGET_ALREADY_EXISTS",
};

View File

@@ -0,0 +1,16 @@
import { readFileSync } from "fs-extra";
import { join } from "path";
export function getFileContent(
this: undefined | { absolutePackageDir?: string },
path: string,
): string | undefined {
if (!this?.absolutePackageDir) {
return undefined;
}
try {
return readFileSync(join(this.absolutePackageDir, path)).toString();
} catch (err) {
return undefined;
}
}

View File

@@ -0,0 +1,17 @@
import type { ProjectTypes } from "@definitions/projectTypes";
import { getProjectType } from "@utils/project";
import { getFilesPathByProject } from "@utils/resource";
export const getPathPrefix = () => {
let projectType: ProjectTypes | undefined = undefined;
try {
projectType = getProjectType();
} catch (error) {
projectType = undefined;
}
const pathPrefix = getFilesPathByProject(projectType);
return pathPrefix;
};

View File

@@ -0,0 +1,256 @@
import { getImports, getNameChangeInImport, reorderImports } from "./import";
describe("getImports", () => {
it("should get all imports", () => {
const content = `
import React from "react";
import { Button } from "antd";
import { TextInput as AntTextInput } from "antd";
import * as Antd from "antd";
import { Button as AntButton, TextInput } from "antd";
import { Button as AntButton, TextInput as AntTextInput } from "antd";
import type { IAuthProvider } from "@refinedev/core";
import { type BaseRecord } from "@refinedev/core";
`;
const expected = [
{
isType: false,
statement: 'import React from "react";',
importPath: "react",
defaultImport: "React",
},
{
isType: false,
statement: 'import { Button } from "antd";',
importPath: "antd",
namedImports: "{ Button }",
},
{
isType: false,
statement: 'import { TextInput as AntTextInput } from "antd";',
importPath: "antd",
namedImports: "{ TextInput as AntTextInput }",
},
{
isType: false,
statement: 'import * as Antd from "antd";',
importPath: "antd",
namespaceImport: "Antd",
},
{
isType: false,
statement: 'import { Button as AntButton, TextInput } from "antd";',
importPath: "antd",
namedImports: "{ Button as AntButton, TextInput }",
},
{
isType: false,
statement:
'import { Button as AntButton, TextInput as AntTextInput } from "antd";',
importPath: "antd",
namedImports: "{ Button as AntButton, TextInput as AntTextInput }",
},
{
isType: true,
statement: 'import type { IAuthProvider } from "@refinedev/core";',
importPath: "@refinedev/core",
namedImports: "{ IAuthProvider }",
},
{
isType: false,
statement: 'import { type BaseRecord } from "@refinedev/core";',
importPath: "@refinedev/core",
namedImports: "{ type BaseRecord }",
},
];
expect(getImports(content)).toEqual(expected);
});
});
describe("getNameChangeInImport", () => {
it("should get all name changes", () => {
const statement = `
{ Button as AntButton, TextInput as AntTextInput, type ButtonProps, type TextInputProps as AntTextInputProps }
`;
const expected = [
{
statement: " Button as AntButton,",
fromName: "Button",
toName: "AntButton",
afterCharacter: ",",
},
{
statement: " TextInput as AntTextInput,",
fromName: "TextInput",
toName: "AntTextInput",
afterCharacter: ",",
},
{
afterCharacter: undefined,
fromName: "type TextInputProps",
statement: " type TextInputProps as AntTextInputProps ",
toName: "AntTextInputProps",
},
];
expect(getNameChangeInImport(statement)).toEqual(expected);
});
});
describe("reorderImports", () => {
it("should reorder named imports", () => {
const content = `
import { Button, TextInput } from "zantd";
import { useEffect } from "react";
import { useList } from "@refinedev/core";
`;
const expected = `
import { useEffect } from "react";
import { useList } from "@refinedev/core";
import { Button, TextInput } from "zantd";
`;
expect(reorderImports(content).trim()).toEqual(expected.trim());
});
it("should merge the same module imports", () => {
const content = `
import { Button, TextInput } from "antd";
import { useEffect } from "react";
import { useList, useOtherList, } from "@refinedev/core";
import { useOne, useOtherOne } from "@refinedev/core";
`;
const expected = `
import { useEffect } from "react";
import { useList, useOtherList, useOne, useOtherOne } from "@refinedev/core";
import { Button, TextInput } from "antd";
`;
expect(reorderImports(content).trim()).toEqual(expected.trim());
});
it("should merge default imports with named imports", () => {
const content = `
import { Button, TextInput } from "antd";
import { useEffect } from "react";
import React from "react";
`;
const expected = `
import React, { useEffect } from "react";
import { Button, TextInput } from "antd";
`;
expect(reorderImports(content).trim()).toEqual(expected.trim());
});
it("should not merge namespace imports with named imports", () => {
const content = `
import { Button, TextInput } from "antd";
import { useEffect } from "react";
import * as Antd from "antd";
`;
const expected = `
import { useEffect } from "react";
import * as Antd from "antd";
import { Button, TextInput } from "antd";
`;
expect(reorderImports(content).trim()).toEqual(expected.trim());
});
it("should not merge namespace imports with default imports", () => {
const content = `
import React from "react";
import * as ReactPackage from "react";
`;
const expected = `
import * as ReactPackage from "react";
import React from "react";
`;
expect(reorderImports(content).trim()).toEqual(expected.trim());
});
it("should keep name changes in named imports", () => {
const content = `
import { Button, TextInput } from "antd";
import { Layout as AntLayout } from "antd";
`;
const expected = `
import { Button, TextInput, Layout as AntLayout } from "antd";
`;
expect(reorderImports(content).trim()).toEqual(expected.trim());
});
it("should keep the imports with comments before", () => {
const content = `
import React from "react";
// comment
import { Button, TextInput } from "antd";
import { Layout } from "antd";
`;
const expected = `
import React from "react";
import { Layout } from "antd";
// comment
import { Button, TextInput } from "antd";
`;
expect(reorderImports(content).trim()).toEqual(expected.trim());
});
it("should keep type imports and add them to the end", () => {
const content = `
import type { Layout } from "antd";
import React from "react";
import { Button, TextInput } from "antd";
`;
const expected = `
import React from "react";
import { Button, TextInput } from "antd";
import type { Layout } from "antd";
`;
expect(reorderImports(content).trim()).toEqual(expected.trim());
});
it("should keep type imports with content", () => {
const content = `
import type { AxiosInstance } from "axios";
import { stringify } from "query-string";
import type { DataProvider } from "@refinedev/core";
import { axiosInstance, generateSort, generateFilter } from "./utils";
type MethodTypes = "get" | "delete" | "head" | "options";
type MethodTypesWithBody = "post" | "put" | "patch";
`;
const expected = `
import { axiosInstance, generateSort, generateFilter } from "./utils";
import { stringify } from "query-string";
import type { AxiosInstance } from "axios";
import type { DataProvider } from "@refinedev/core";
type MethodTypes = "get" | "delete" | "head" | "options";
type MethodTypesWithBody = "post" | "put" | "patch";
`;
expect(reorderImports(content).trim()).toEqual(expected.trim());
});
});

View File

@@ -0,0 +1,266 @@
const packageRegex =
/import(?:\s+(type))?\s*(?:([^\s\{\},]+)\s*(?:,\s*)?)?(\{[^}]+\})?\s*(?:\*\s*as\s+([^\s\{\}]+)\s*)?from\s*['"]([^'"]+)['"];?/g;
const nameChangeRegex = /((?:\w|\s|_)*)( as )((?:\w|\s|_)*)( |,)?/g;
export type ImportMatch = {
statement: string;
importPath: string;
defaultImport?: string;
namedImports?: string;
namespaceImport?: string;
isType?: boolean;
};
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,
typePrefix,
defaultImport,
namedImports,
namespaceImport,
importPath,
] = match;
imports.push({
isType: typePrefix === "type",
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.isType,
);
const typeImports = allImports.filter((importMatch) => importMatch.isType);
const importsWithBeforeContent: ImportMatch[] = [];
const importsWithoutBeforeContent: ImportMatch[] = [];
// // remove all type imports
typeImports.forEach((importMatch) => {
newContent = newContent.replace(`${importMatch.statement}\n`, "");
});
allModuleImports.forEach((importMatch) => {
if (isImportHasBeforeContent(newContent, 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, "");
});
// 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;
};

View File

@@ -0,0 +1,23 @@
import path from "path";
import type { RefineConfig } from "@definitions";
import { provideCliHelpers } from "./provideCliHelpers";
export const getRefineConfig = async (
packagePath: string,
isAbsolute?: boolean,
) => {
try {
provideCliHelpers(packagePath, isAbsolute);
const config = require(
path.join(
isAbsolute ? packagePath : path.join(process.cwd(), packagePath),
"refine.config.js",
),
) as RefineConfig;
return config;
} catch (error) {
return undefined;
}
};

View File

@@ -0,0 +1,25 @@
import { parseSwizzleBlocks } from "./parseSwizzleBlocks";
describe("parseSwizzleBlocks", () => {
it("should remove swizzle blocks", () => {
const content = `
1
// swizzle-remove-start
remove-this
//swizzle-remove-end
2
/* swizzle-remove-start */
remove-this-too
/* swizzle-remove-end */
`;
const result = parseSwizzleBlocks(content);
expect(result).not.toContain("remove-this");
expect(result).not.toContain("remove-this-too");
expect(result).toContain("1");
expect(result).toContain("2");
expect(result).not.toContain("swizzle-remove-start");
expect(result).not.toContain("swizzle-remove-end");
});
});

View File

@@ -0,0 +1,6 @@
export const parseSwizzleBlocks = (content: string) => {
const regex =
/(\/\/|\/\*)(\s?)swizzle-remove-start([\s\S]*?)(\/\/|\/\*)(\s?)swizzle-remove-end(\s*)(\*\/)?/g;
return content.replace(regex, "");
};

View File

@@ -0,0 +1,14 @@
import { format, resolveConfig } from "prettier";
export const prettierFormat = async (code: string) => {
try {
const prettierConfig = await resolveConfig(process.cwd());
return format(code, {
...(prettierConfig ?? {}),
parser: "typescript",
});
} catch (err) {
return code;
}
};

View File

@@ -0,0 +1,32 @@
import path from "path";
import * as RefineCLI from "../../index";
import { getFileContent } from "./getFileContent";
const Module = require("module");
const originalRequire = Module.prototype.require;
export const provideCliHelpers = (
packagePath: string,
isAbsolute?: boolean,
) => {
Module.prototype.require = function (...args: Parameters<NodeRequire>) {
if ((args[0] as unknown as string) === "@refinedev/cli") {
return {
...RefineCLI,
getFileContent: (filePath: string) => {
return getFileContent.call(
{
absolutePackageDir: isAbsolute
? packagePath
: path.join(process.cwd(), packagePath),
},
filePath,
);
},
};
}
//do your thing here
return originalRequire.apply(this, args);
};
};

View File

@@ -0,0 +1,46 @@
import chalk from "chalk";
import cardinal from "cardinal";
import boxen from "boxen";
const getCodeData = (content: string): { title?: string; code: string } => {
const titleRegexp = /^(?:\/\/\s?title:\s?)(.*?)\n/g;
const [commentLine, titleMatch] = titleRegexp.exec(content) ?? [];
if (titleMatch) {
const title = titleMatch.trim();
const code = content.replace(commentLine || "", "");
return { title, code };
}
return { code: content };
};
export const renderCodeMarkdown = (content: string) => {
const { title, code: rawCode } = getCodeData(content);
let highlighted = "";
// run cardinal on codeContent
try {
const code = cardinal.highlight(rawCode, {
jsx: true,
});
highlighted = code;
} catch (err) {
highlighted = rawCode;
}
// wrap to boxen
const boxed = boxen(highlighted, {
padding: 1,
margin: 0,
borderStyle: "round",
borderColor: "gray",
titleAlignment: "left",
title: title ? chalk.bold(title) : undefined,
});
return boxed;
};