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,81 @@
import React from "react";
import matter from "gray-matter";
import boxen from "boxen";
import type { Announcement } from "@definitions/announcement";
import { markedTerminalRenderer } from "@utils/marked-terminal-renderer";
const ANNOUNCEMENT_URL =
"https://raw.githubusercontent.com/refinedev/refine/master/packages/cli/ANNOUNCEMENTS.md";
const ANNOUNCEMENT_DELIMITER = "---announcement";
const splitAnnouncements = (feed: string) => {
const sections = feed.split(ANNOUNCEMENT_DELIMITER);
return sections
.slice(1)
.map((section) => `${ANNOUNCEMENT_DELIMITER}${section}`);
};
const parseAnnouncement = (raw: string): Announcement => {
const fixed = raw.replace(ANNOUNCEMENT_DELIMITER, "---");
const parsed = matter(fixed);
const content = (
parsed.content.length === 0
? fixed.replace(/---/g, "")
: parsed.content.replace(/---/g, "")
).trim();
return {
...parsed.data,
content,
} as Announcement;
};
export const getAnnouncements = async () => {
try {
const response = await fetch(ANNOUNCEMENT_URL)
.then((res) => res.text())
.catch(() => "");
const announcements = splitAnnouncements(response).map((section) =>
parseAnnouncement(section),
);
return announcements;
} catch (_) {
return [];
}
};
export const printAnnouncements = async () => {
const announcements = await getAnnouncements();
const visibleAnnouncements = announcements.filter(
(a) => Boolean(a.hidden) === false,
);
if (visibleAnnouncements.length === 0) {
return;
}
const stringAnnouncements = visibleAnnouncements
.map((a) => {
const dash = visibleAnnouncements.length > 1 ? "— " : "";
const content = markedTerminalRenderer(a.content);
return `${dash}${content}`;
})
.join("")
.trim();
console.log(
boxen(stringAnnouncements, {
padding: 1,
margin: 0,
borderStyle: "round",
borderColor: "blueBright",
titleAlignment: "left",
}),
);
};

View File

@@ -0,0 +1,24 @@
import { findDuplicates } from "@utils/array";
test("Find duplicates from array", () => {
const testCases = [
{
input: [],
output: [],
},
{
input: [1, 2, 3, 3, "3", "3"],
output: [3, "3"],
},
{
input: [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5],
output: [1, 2, 3, 4, 5],
},
];
testCases.forEach((testCase) => {
const result = findDuplicates(testCase.input);
expect(result).toEqual(testCase.output);
});
});

View File

@@ -0,0 +1,5 @@
export const findDuplicates = (arr: (string | number)[]) => {
const duplicates = arr.filter((item, index) => arr.indexOf(item) !== index);
const unique = new Set(duplicates);
return Array.from(unique);
};

View File

@@ -0,0 +1,193 @@
import type {
ASTPath,
Collection,
ImportDeclaration,
JSCodeshift,
JSXAttribute,
JSXElement,
JSXExpressionContainer,
} from "jscodeshift";
export const wrapElement = (
j: JSCodeshift,
source: Collection,
element: ASTPath<JSXElement>,
wrapper: string,
wrapperAttributes: JSXAttribute[] = [],
) => {
const existingWrapperElement = source.find(j.JSXElement, {
openingElement: { name: { name: wrapper } },
});
if (existingWrapperElement.length) {
return element;
}
const wrapperElement = j.jsxElement(
j.jsxOpeningElement(j.jsxIdentifier(wrapper), wrapperAttributes),
j.jsxClosingElement(j.jsxIdentifier(wrapper)),
[
j.jsxElement(
element.node.openingElement,
element.node.closingElement,
element.node.children,
),
],
);
j(element).replaceWith(wrapperElement);
return element;
};
export const wrapChildren = (
j: JSCodeshift,
source: Collection,
element: ASTPath<JSXElement>,
wrapper: string,
wrapperAttributes: JSXAttribute[] = [],
) => {
const existingWrapperElement = source.find(j.JSXElement, {
openingElement: { name: { name: wrapper } },
});
if (existingWrapperElement.length) {
return element;
}
const wrapperElement = j.jsxElement(
j.jsxOpeningElement(j.jsxIdentifier(wrapper), wrapperAttributes),
j.jsxClosingElement(j.jsxIdentifier(wrapper)),
element.value.children,
);
j(element).replaceWith(
j.jsxElement(element.node.openingElement, element.node.closingElement, [
wrapperElement,
]),
);
return element;
};
export const addAttributeIfNotExist = (
j: JSCodeshift,
source: Collection,
element: ASTPath<JSXElement>,
attributeIdentifier: string,
attributeValue?: JSXElement | JSXExpressionContainer,
) => {
const existingAttribute = source.find(j.JSXAttribute, {
name: {
name: attributeIdentifier,
},
});
if (existingAttribute.length) {
return;
}
const attribute = j.jsxAttribute(
j.jsxIdentifier(attributeIdentifier),
attributeValue ? attributeValue : undefined,
);
element.node.openingElement.attributes?.push(attribute);
};
export const addOrUpdateImports = (
j: JSCodeshift,
source: Collection,
importPath: string,
importIdentifierNames: string[],
insertFunc?: (
sourceDeclaration: Collection<ImportDeclaration>,
targetDeclaration: ImportDeclaration,
) => void,
isDefault = false,
) => {
const existingImports = source.find(j.ImportDeclaration, {
source: {
value: importPath,
},
});
if (isDefault && existingImports.length > 0) {
return;
}
const specifierFunc = isDefault
? j.importDefaultSpecifier
: j.importSpecifier;
const importSpecifiers = importIdentifierNames.map((importIdentifierName) =>
specifierFunc(j.identifier(importIdentifierName)),
);
if (existingImports.length) {
// Check existing imports in the `ImportDeclaration` to avoid duplicate imports
const nonExistingImportIdentifiers = importIdentifierNames.filter(
(importIdentifierName) =>
existingImports.find(j.ImportSpecifier).filter((path) => {
return path.node.imported.name === importIdentifierName.split(" ")[0];
}).length === 0,
);
if (nonExistingImportIdentifiers.length === 0) {
return;
}
const nonExistingSpecifiers = nonExistingImportIdentifiers.map(
(importIdentifierName) =>
specifierFunc(j.identifier(importIdentifierName)),
);
existingImports
.at(0)
.get("specifiers")
.value.push(...nonExistingSpecifiers);
} else {
const importDeclaration = j.importDeclaration(
importSpecifiers,
j.literal(importPath),
);
insertFunc?.(source.find(j.ImportDeclaration), importDeclaration);
}
};
export const addOrUpdateNamelessImport = (
j: JSCodeshift,
source: Collection,
importPath: string,
insertFunc: (
sourceDeclaration: Collection<ImportDeclaration>,
targetDeclaration: ImportDeclaration,
) => void,
) => {
const existingImports = source.find(j.ImportDeclaration, {
source: {
value: importPath,
},
});
if (existingImports.length) {
return;
}
const importDeclaration = j.importDeclaration([], j.literal(importPath));
insertFunc(source.find(j.ImportDeclaration), importDeclaration);
};
export const removeImportIfExists = (
j: JSCodeshift,
source: Collection,
importPath: string,
importIdentifierName: string,
) => {
source
.find(j.ImportDeclaration, { source: { value: importPath } })
.find(j.ImportSpecifier, { imported: { name: importIdentifierName } })
.remove();
};

View File

@@ -0,0 +1,76 @@
import Handlebars from "handlebars";
import {
readFileSync,
readdirSync,
createFileSync,
writeFileSync,
unlinkSync,
} from "fs-extra";
import { getComponentNameByResource } from "@utils/resource";
export const compile = (filePath: string, params: any): string => {
const content = readFileSync(filePath);
Handlebars.registerHelper("ifIn", (elem, list, options) => {
if (elem.includes(list)) {
return options.fn(Handlebars);
}
return options.inverse(Handlebars);
});
Handlebars.registerHelper("formatInferencerComponent", (string) => {
if (!string) {
return;
}
switch (string) {
case "chakra-ui":
return "ChakraUI";
default:
return string.charAt(0).toUpperCase() + string.slice(1);
}
});
Handlebars.registerHelper("capitalize", (string) => {
if (!string) {
return;
}
return string.charAt(0).toUpperCase() + string.slice(1);
});
Handlebars.registerHelper("getComponentNameByResource", (string) => {
if (!string) {
return;
}
return getComponentNameByResource(string);
});
const template = Handlebars.compile(content.toString());
return template(params);
};
/**
* compile all hbs files under the specified directory. recursively
*/
export const compileDir = (dirPath: string, params: any) => {
const files = readdirSync(dirPath, { recursive: true });
files.forEach((file: string | Buffer) => {
// the target file should be a handlebars file
if (typeof file !== "string" || !file.endsWith(".hbs")) return;
const templateFilePath = `${dirPath}/${file}`;
// create file
const compiledFilePath = `${dirPath}/${file.replace(".hbs", "")}`;
createFileSync(compiledFilePath);
// write compiled file
writeFileSync(compiledFilePath, compile(templateFilePath, params));
// delete template file (*.hbs)
unlinkSync(templateFilePath);
});
};

View File

@@ -0,0 +1,14 @@
export const stringToBase64 = (str: string) => {
if (typeof btoa !== "undefined") {
return btoa(str);
}
return Buffer.from(str).toString("base64");
};
export const base64ToString = (base64: string) => {
if (typeof atob !== "undefined") {
return atob(base64);
}
return Buffer.from(base64, "base64").toString();
};

View File

@@ -0,0 +1,55 @@
import { getNodeEnv } from ".";
test("Get NODE_ENV", async () => {
const testCases = [
{
input: "development",
expected: "development",
},
{
input: "dev",
expected: "development",
},
{
input: "Production",
expected: "production",
},
{
input: "prod",
expected: "production",
},
{
input: "test",
expected: "test",
},
{
input: "TESTING",
expected: "test",
},
{
input: "ci",
expected: "continuous-integration",
},
{
input: "UAT",
expected: "user-acceptance-testing",
},
{
input: "SIT",
expected: "system-integration-testing",
},
{
input: "another-node-env",
expected: "custom",
},
{
input: "",
expected: "development",
},
];
for (const testCase of testCases) {
process.env.NODE_ENV = testCase.input;
expect(getNodeEnv()).toEqual(testCase.expected);
}
});

48
packages/cli/src/utils/env/index.ts vendored Normal file
View File

@@ -0,0 +1,48 @@
import type { NODE_ENV } from "@definitions/node";
import * as dotenv from "dotenv";
const refineEnv: Record<string, string> = {};
dotenv.config({ processEnv: refineEnv });
const envSearchMap: Record<Exclude<NODE_ENV, "custom">, RegExp> = {
development: /dev/i,
production: /prod/i,
test: /test|tst/i,
"continuous-integration": /ci/i,
"user-acceptance-testing": /uat/i,
"system-integration-testing": /sit/i,
};
export const getNodeEnv = (): NODE_ENV => {
const nodeEnv = process.env.NODE_ENV;
if (!nodeEnv) {
return "development";
}
let env: NODE_ENV = "custom";
for (const [key, value] of Object.entries(envSearchMap)) {
if (value.test(nodeEnv)) {
env = key as NODE_ENV;
break;
}
}
return env;
};
const getEnvValue = (key: string): string | undefined => {
return process.env[key] || refineEnv[key];
};
export const ENV = {
NODE_ENV: getNodeEnv(),
REFINE_NO_TELEMETRY: getEnvValue("REFINE_NO_TELEMETRY") || "false",
UPDATE_NOTIFIER_IS_DISABLED:
getEnvValue("UPDATE_NOTIFIER_IS_DISABLED") || "false",
UPDATE_NOTIFIER_CACHE_TTL:
getEnvValue("UPDATE_NOTIFIER_CACHE_TTL") || 1000 * 60 * 60 * 24,
REFINE_DEVTOOLS_PORT: getEnvValue("REFINE_DEVTOOLS_PORT"),
};

View File

@@ -0,0 +1,9 @@
import { renderCodeMarkdown } from "@utils/swizzle/renderCodeMarkdown";
import { marked } from "marked";
import TerminalRenderer from "marked-terminal";
export const markedTerminalRenderer = (markdown: string) => {
return marked(markdown, {
renderer: new TerminalRenderer({ code: renderCodeMarkdown }) as any,
});
};

View File

@@ -0,0 +1,25 @@
import envinfo from "envinfo";
import os from "os";
export const getOSType = () => {
const osPlatform = os.type();
const types: Record<string, "macOS" | "Linux" | "Windows"> = {
Darwin: "macOS",
Linux: "Linux",
Windows_NT: "Windows",
};
return types[osPlatform];
};
export const getOS = async () => {
// returns as a ['OS', 'macOS Mojave 10.14.5']
const [_, OSInfo] =
(await envinfo.helpers.getOSInfo()) as unknown as string[];
return {
name: getOSType(),
version: OSInfo,
};
};

View File

@@ -0,0 +1,46 @@
import { parsePackageNameAndVersion } from "@utils/package";
test("Get package name and version from string", () => {
const testCases = [
{
input: "@refinedev/antd@2.36.2",
output: {
name: "@refinedev/antd",
version: "2.36.2",
},
},
{
input: "@owner/package_name@2.36.2_beta.1",
output: {
name: "@owner/package_name",
version: "2.36.2_beta.1",
},
},
{
input: "@owner/package-name",
output: {
name: "@owner/package-name",
version: null,
},
},
{
input: "owner/package-name",
output: {
name: "owner/package-name",
version: null,
},
},
{
input: "owner/package-name@3.2.1",
output: {
name: "owner/package-name",
version: "3.2.1",
},
},
];
testCases.forEach((testCase) => {
const result = parsePackageNameAndVersion(testCase.input);
expect(result).toEqual(testCase.output);
});
});

View File

@@ -0,0 +1,347 @@
import spinner from "@utils/spinner";
import execa from "execa";
import { existsSync, pathExists, readFileSync, readJSON } from "fs-extra";
import globby from "globby";
import path from "path";
import preferredPM from "preferred-pm";
import type { PackageJson } from "@definitions/package";
export const getPackageJson = (): PackageJson => {
if (!existsSync("package.json")) {
console.error("❌ `package.json` not found.");
throw new Error("./package.json not found");
}
return JSON.parse(readFileSync("package.json", "utf8"));
};
export const getDependencies = () => {
const packageJson = getPackageJson();
return Object.keys(packageJson.dependencies || {});
};
export const getDependenciesWithVersion = () => {
const packageJson = getPackageJson();
return packageJson?.dependencies || {};
};
export const getDevDependencies = () => {
const packageJson = getPackageJson();
return Object.keys(packageJson.devDependencies || {});
};
export const getAllDependencies = () => {
return [...getDependencies(), ...getDependencies()];
};
export const getScripts = () => {
const packageJson = getPackageJson();
return packageJson?.scripts || {};
};
export const getInstalledRefinePackages = async () => {
try {
const execution = await execa("npm", ["ls", "--depth=0", "--json"], {
reject: false,
});
const dependencies = JSON.parse(execution.stdout)?.dependencies || {};
const refineDependencies = Object.keys(dependencies).filter(
(dependency) =>
dependency.startsWith("@refinedev") ||
dependency.startsWith("@pankod/refine-"),
);
const normalize: {
name: string;
version: string;
}[] = [];
for (const dependency of refineDependencies) {
const version = dependencies[dependency].version;
normalize.push({
name: dependency,
version,
});
}
return normalize;
} catch (error) {
return Promise.resolve(null);
}
};
export const getInstalledRefinePackagesFromNodeModules = async () => {
const REFINE_PACKAGES = ["core"];
try {
const packagesFromGlobbySearch = await globby("node_modules/@refinedev/*", {
onlyDirectories: true,
});
const packageDirsFromModules = REFINE_PACKAGES.flatMap((pkg) => {
try {
const pkgPath = require.resolve(
path.join("@refinedev", pkg, "package.json"),
);
return [path.dirname(pkgPath)];
} catch (err) {
return [];
}
});
const refinePackages: Array<{
name: string;
path: string;
version: string;
}> = [];
await Promise.all(
[...packageDirsFromModules, ...packagesFromGlobbySearch].map(
async (packageDir) => {
const hasPackageJson = await pathExists(`${packageDir}/package.json`);
if (hasPackageJson) {
const packageJson = await readJSON(`${packageDir}/package.json`);
refinePackages.push({
name: packageJson.name,
version: packageJson.version,
path: packageDir,
});
}
},
),
);
return refinePackages;
} catch (err) {
return [];
}
};
export const isPackageHaveRefineConfig = async (packagePath: string) => {
return await pathExists(`${packagePath}/refine.config.js`);
};
export const pmCommands = {
npm: {
add: ["install", "--save"],
addDev: ["install", "--save-dev"],
outdatedJson: ["outdated", "--json"],
install: ["install"],
},
yarn: {
add: ["add"],
addDev: ["add", "-D"],
outdatedJson: ["outdated", "--json"],
install: ["install"],
},
pnpm: {
add: ["add"],
addDev: ["add", "-D"],
outdatedJson: ["outdated", "--format", "json"],
install: ["install"],
},
bun: {
add: ["add"],
addDev: ["add", "--dev"],
outdatedJson: ["outdated", "--format", "json"],
install: ["install"],
},
};
export const getPreferedPM = async () => {
const pm = await spinner(
() => preferredPM(process.cwd()),
"Getting package manager...",
);
if (!pm) {
throw new Error("Package manager not found.");
}
return pm;
};
export const installPackages = async (
packages: string[],
type: "all" | "add" = "all",
successMessage = "All `Refine` packages updated 🎉",
) => {
const pm = await getPreferedPM();
try {
const installCommand =
type === "all" ? pmCommands[pm.name].install : pmCommands[pm.name].add;
const execution = execa(pm.name, [...installCommand, ...packages], {
stdio: "inherit",
});
execution.on("message", (message) => {
console.log(message);
});
execution.on("error", (error) => {
console.log(error);
});
execution.on("exit", (exitCode) => {
if (exitCode === 0) {
console.log(successMessage);
return;
}
console.log(`Application exited with code ${exitCode}`);
});
} catch (error: any) {
throw new Error(error);
}
};
export const installPackagesSync = async (packages: string[]) => {
const pm = await getPreferedPM();
try {
const installCommand = pmCommands[pm.name].add;
const execution = execa.sync(pm.name, [...installCommand, ...packages], {
stdio: "inherit",
});
if (execution.failed || execution.exitCode !== 0) {
throw new Error(execution.stderr);
}
return execution;
} catch (error: any) {
throw new Error(error);
}
};
export interface PackageNameAndVersion {
name: string;
version: string | null;
}
export const parsePackageNameAndVersion = (
str: string,
): PackageNameAndVersion => {
const versionStartIndex = str.lastIndexOf("@");
if (versionStartIndex <= 0) {
return {
name: str,
version: null,
};
}
return {
name: str.slice(0, versionStartIndex),
version: str.slice(versionStartIndex + 1),
};
};
export const getRefineProjectId = () => {
const packageJson = getPackageJson();
return packageJson?.refine?.projectId;
};
export const isDevtoolsInstalled = async () => {
const installedPackages = await getInstalledRefinePackagesFromNodeModules();
return installedPackages.some((pkg) => pkg.name === "@refinedev/devtools");
};
export const getNotInstalledPackages = (packages: string[]) => {
const dependencies = getDependencies();
return packages.filter((pkg) => !dependencies.includes(pkg));
};
export const installMissingPackages = async (packages: string[]) => {
console.log("🌱 Checking dependencies...");
const missingPackages = getNotInstalledPackages(packages);
if (missingPackages.length > 0) {
console.log(`🌱 Installing ${missingPackages.join(", ")}`);
await installPackagesSync(missingPackages);
console.log("🎉 Installation complete...");
} else {
console.log("🎉 All required packages are already installed");
}
};
export const hasIncomatiblePackages = (packages: string[]): boolean => {
const allDependencies = getAllDependencies();
const incompatiblePackages = packages.filter((pkg) =>
allDependencies.includes(pkg),
);
if (incompatiblePackages.length > 0) {
console.log(
`🚨 This feature doesn't support ${incompatiblePackages.join(
", ",
)} package.`,
);
return true;
}
return false;
};
export const getAllVersionsOfPackage = async (
packageName: string,
): Promise<string[]> => {
const pm = "npm";
const { stdout, timedOut } = await execa(
pm,
["view", packageName, "versions", "--json"],
{
reject: false,
timeout: 25 * 1000,
},
);
if (timedOut) {
console.log("❌ Timed out while checking for updates.");
process.exit(1);
}
let result:
| string[]
| {
error: {
code: string;
};
} = [];
try {
result = JSON.parse(stdout);
if (!result || "error" in result) {
console.log("❌ Something went wrong while checking for updates.");
process.exit(1);
}
} catch (error) {
console.log("❌ Something went wrong while checking for updates.");
process.exit(1);
}
return result;
};
export const isInstalled = async (packageName: string) => {
const installedPackages = await getInstalledRefinePackages();
if (!installedPackages) {
return false;
}
return installedPackages.some((pkg) => pkg.name === packageName);
};

View File

@@ -0,0 +1,104 @@
import { ProjectTypes, UIFrameworks } from "@definitions";
import { getDependencies, getDevDependencies } from "@utils/package";
export const getProjectType = (platform?: ProjectTypes): ProjectTypes => {
if (platform) {
return platform;
}
// read dependencies from package.json
let dependencies: string[] = [];
let devDependencies: string[] = [];
try {
dependencies = getDependencies();
devDependencies = getDevDependencies();
} catch (error) {}
// check for craco
// craco and react-scripts installs together. We need to check for craco first
if (
dependencies.includes("@craco/craco") ||
devDependencies.includes("@craco/craco")
) {
return ProjectTypes.CRACO;
}
// check for react-scripts
if (
dependencies.includes("react-scripts") ||
devDependencies.includes("react-scripts")
) {
return ProjectTypes.REACT_SCRIPT;
}
// check for next
if (dependencies.includes("next") || devDependencies.includes("next")) {
return ProjectTypes.NEXTJS;
}
// check for remix
if (
dependencies.includes("@remix-run/react") ||
devDependencies.includes("@remix-run/react")
) {
// check for remix-vite
if (dependencies.includes("vite") || devDependencies.includes("vite")) {
return ProjectTypes.REMIX_VITE;
}
return ProjectTypes.REMIX;
}
// check for vite
if (dependencies.includes("vite") || devDependencies.includes("vite")) {
return ProjectTypes.VITE;
}
if (dependencies.includes("parcel") || devDependencies.includes("parcel")) {
return ProjectTypes.PARCEL;
}
return ProjectTypes.UNKNOWN;
};
export const getUIFramework = (): UIFrameworks | undefined => {
// read dependencies from package.json
const dependencies = getDependencies();
// check for antd
if (dependencies.includes("@refinedev/antd")) {
return UIFrameworks.ANTD;
}
// check for mui
if (dependencies.includes("@refinedev/mui")) {
return UIFrameworks.MUI;
}
// check for chakra
if (dependencies.includes("@refinedev/chakra-ui")) {
return UIFrameworks.CHAKRA;
}
// check for mantine
if (dependencies.includes("@refinedev/mantine")) {
return UIFrameworks.MANTINE;
}
return;
};
export const getDevtoolsEnvKeyByProjectType = (
projectType: ProjectTypes,
): string => {
switch (projectType) {
case ProjectTypes.REACT_SCRIPT:
return "REACT_APP_REFINE_DEVTOOLS_PORT";
case ProjectTypes.NEXTJS:
return "NEXT_PUBLIC_REFINE_DEVTOOLS_PORT";
case ProjectTypes.VITE:
return "VITE_REFINE_DEVTOOLS_PORT";
default:
return "REFINE_DEVTOOLS_PORT";
}
};

View File

@@ -0,0 +1,69 @@
import * as utilsPackage from "@utils/package";
import { hasDefaultScript } from ".";
test("Has default script", () => {
const testCases = [
{
input: {
scripts: {
dev: "refine dev",
},
},
output: {
dev: true,
},
},
{
input: {
scripts: {
dev: "PORT=5252 refine dev --force",
},
},
output: {
dev: true,
},
},
{
input: {
scripts: {
dev: "refine dev",
},
},
output: {
dev: true,
},
},
{
input: {
scripts: {
dev: "refine dev2",
},
},
output: {
dev: false,
},
},
{
input: {
scripts: {
dev: "refine dev;echo '1'",
},
},
output: {
dev: true,
},
},
];
testCases.forEach((testCase) => {
jest.spyOn(utilsPackage, "getPackageJson").mockReturnValueOnce({
name: "test",
version: "1.0.0",
...testCase.input,
});
const result = hasDefaultScript();
expect(result).toEqual(testCase.output);
});
});

View File

@@ -0,0 +1,21 @@
import { getPackageJson } from "@utils/package";
type ReturnType = {
dev: boolean;
};
/**
* Checks if the project has a refine script in package.json
*/
export const hasDefaultScript = (): ReturnType => {
const packageJson = getPackageJson();
const scripts = packageJson.scripts || {};
const isDefault =
((scripts?.dev || "") as string).match(/refine dev(\s|$|;){1}/) !== null;
return {
dev: isDefault,
};
};

View File

@@ -0,0 +1,49 @@
import { ProjectTypes } from "@definitions/projectTypes";
import { getProviderPath } from ".";
it("should get provider path", () => {
expect(getProviderPath(ProjectTypes.NEXTJS)).toEqual({
path: "src/providers",
alias: "../src/providers",
});
expect(getProviderPath(ProjectTypes.REMIX)).toEqual({
path: "app/providers",
alias: "~/providers",
});
expect(getProviderPath(ProjectTypes.REMIX_VITE)).toEqual({
path: "app/providers",
alias: "~/providers",
});
expect(getProviderPath(ProjectTypes.REMIX_SPA)).toEqual({
path: "app/providers",
alias: "~/providers",
});
expect(getProviderPath(ProjectTypes.VITE)).toEqual({
path: "src/providers",
alias: "providers",
});
expect(getProviderPath(ProjectTypes.REACT_SCRIPT)).toEqual({
path: "src/providers",
alias: "providers",
});
expect(getProviderPath(ProjectTypes.CRACO)).toEqual({
path: "src/providers",
alias: "providers",
});
expect(getProviderPath(ProjectTypes.PARCEL)).toEqual({
path: "src/providers",
alias: "providers",
});
expect(getProviderPath(ProjectTypes.UNKNOWN)).toEqual({
path: "src/providers",
alias: "providers",
});
});

View File

@@ -0,0 +1,70 @@
import { ProjectTypes } from "@definitions/projectTypes";
import camelCase from "camelcase";
export const getResourcePath = (
projectType: ProjectTypes,
): { path: string; alias: string } => {
switch (projectType) {
case ProjectTypes.NEXTJS:
return {
path: "src/components",
alias: "../src/components",
};
case ProjectTypes.REMIX:
case ProjectTypes.REMIX_VITE:
case ProjectTypes.REMIX_SPA:
return {
path: "app/components",
alias: "~/components",
};
}
// vite and react
return {
path: "src/pages",
alias: "pages",
};
};
export const getProviderPath = (
projectType: ProjectTypes,
): { path: string; alias: string } => {
switch (projectType) {
case ProjectTypes.NEXTJS:
return {
path: "src/providers",
alias: "../src/providers",
};
case ProjectTypes.REMIX:
case ProjectTypes.REMIX_VITE:
case ProjectTypes.REMIX_SPA:
return {
path: "app/providers",
alias: "~/providers",
};
}
// vite and react
return {
path: "src/providers",
alias: "providers",
};
};
export const getFilesPathByProject = (projectType?: ProjectTypes) => {
switch (projectType) {
case ProjectTypes.REMIX:
case ProjectTypes.REMIX_VITE:
case ProjectTypes.REMIX_SPA:
return "./app";
default:
return "./src";
}
};
export const getComponentNameByResource = (resource: string): string => {
return camelCase(resource, {
preserveConsecutiveUppercase: true,
pascalCase: true,
});
};

View File

@@ -0,0 +1,13 @@
import ora from "ora";
const spinner = async <T>(fn: () => Promise<T>, message: string) => {
const spinner = ora({
color: "cyan",
text: message,
}).start();
const result = await fn();
spinner.stop();
return result;
};
export default spinner;

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;
};

View File

@@ -0,0 +1,29 @@
import { removeANSIColors, uppercaseFirstChar } from ".";
describe("uppercaseFirstChar", () => {
it("should return the string with the first character capitalized", () => {
const str = "hello world";
const result = uppercaseFirstChar(str);
expect(result).toEqual("Hello world");
});
it("should return an empty string if the input is empty", () => {
const str = "";
const result = uppercaseFirstChar(str);
expect(result).toEqual("");
});
});
describe("removeANSIColors", () => {
it("should remove ANSI color codes from the string", () => {
const str = "\u001b[31mHello \u001b[32mworld\u001b[0m";
const result = removeANSIColors(str);
expect(result).toEqual("Hello world");
});
it("should return the original string if it does not contain any ANSI color codes", () => {
const str = "Hello world";
const result = removeANSIColors(str);
expect(result).toEqual(str);
});
});

View File

@@ -0,0 +1,11 @@
export const uppercaseFirstChar = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
export const removeANSIColors = (str: string): string => {
return str.replace(
// biome-ignore lint/suspicious/noControlCharactersInRegex: we want to remove invisible characters here.
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
"",
);
};