mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
packages
This commit is contained in:
280
packages/cli/src/commands/swizzle/index.tsx
Normal file
280
packages/cli/src/commands/swizzle/index.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import React from "react";
|
||||
import path from "path";
|
||||
import chalk from "chalk";
|
||||
import inquirer from "inquirer";
|
||||
import type { Command, OptionValues } from "commander";
|
||||
import inquirerAutoCompletePrompt from "inquirer-autocomplete-prompt";
|
||||
import { ensureFile, pathExists, readFile, writeFile } from "fs-extra";
|
||||
|
||||
import { getRefineConfig } from "@utils/swizzle";
|
||||
import { prettierFormat } from "@utils/swizzle/prettierFormat";
|
||||
import {
|
||||
getInstalledRefinePackagesFromNodeModules,
|
||||
isPackageHaveRefineConfig,
|
||||
} from "@utils/package";
|
||||
|
||||
import { printSwizzleMessage } from "@components/swizzle-message";
|
||||
|
||||
import type { SwizzleFile } from "@definitions";
|
||||
import { parseSwizzleBlocks } from "@utils/swizzle/parseSwizzleBlocks";
|
||||
import { reorderImports } from "@utils/swizzle/import";
|
||||
import { SWIZZLE_CODES } from "@utils/swizzle/codes";
|
||||
import boxen from "boxen";
|
||||
import { getPathPrefix } from "@utils/swizzle/getPathPrefix";
|
||||
import { installRequiredPackages } from "./install-required-packages";
|
||||
|
||||
const swizzle = (program: Command) => {
|
||||
return program
|
||||
.command("swizzle")
|
||||
.description(
|
||||
`Export a component or a function from ${chalk.bold(
|
||||
"Refine",
|
||||
)} packages to customize it in your project`,
|
||||
)
|
||||
.action(action);
|
||||
};
|
||||
|
||||
const getAutocompleteSource =
|
||||
(
|
||||
rawList: Array<{
|
||||
label: string;
|
||||
group?: string;
|
||||
value?: Record<string, unknown>;
|
||||
}>,
|
||||
) =>
|
||||
(_answers: {}, input = "") => {
|
||||
const filtered = rawList.filter(
|
||||
(el) =>
|
||||
el.label.toLowerCase().includes(input.toLowerCase()) ||
|
||||
el.group?.toLowerCase().includes(input.toLowerCase()),
|
||||
);
|
||||
|
||||
return filtered.flatMap((component, index, arr) => {
|
||||
const hasTitle =
|
||||
component?.group && arr[index - 1]?.group !== component.group;
|
||||
const withTitle =
|
||||
hasTitle && component.group
|
||||
? [new inquirer.Separator(chalk.bold(component.group))]
|
||||
: [];
|
||||
|
||||
return [
|
||||
...withTitle,
|
||||
{
|
||||
name: ` ${component.label}`,
|
||||
value: component?.value ? component.value : component,
|
||||
},
|
||||
];
|
||||
});
|
||||
};
|
||||
|
||||
const action = async (_options: OptionValues) => {
|
||||
inquirer.registerPrompt("autocomplete", inquirerAutoCompletePrompt);
|
||||
|
||||
const installedPackages = await getInstalledRefinePackagesFromNodeModules();
|
||||
|
||||
const packagesWithConfig: Array<{ name: string; path: string }> = [];
|
||||
|
||||
await Promise.all(
|
||||
installedPackages.map(async (pkg) => {
|
||||
const hasConfig = await isPackageHaveRefineConfig(pkg.path);
|
||||
const isNotDuplicate =
|
||||
packagesWithConfig.findIndex((el) => el.name === pkg.name) === -1;
|
||||
if (hasConfig && isNotDuplicate) {
|
||||
packagesWithConfig.push(pkg);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (packagesWithConfig.length === 0) {
|
||||
console.log("No Refine packages found with swizzle configuration.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${boxen(
|
||||
`Found ${chalk.blueBright(
|
||||
packagesWithConfig.length,
|
||||
)} installed ${chalk.blueBright.bold(
|
||||
"Refine",
|
||||
)} packages with swizzle configuration.`,
|
||||
{
|
||||
padding: 1,
|
||||
textAlignment: "center",
|
||||
dimBorder: true,
|
||||
borderColor: "blueBright",
|
||||
borderStyle: "round",
|
||||
},
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
const packageConfigs = await Promise.all(
|
||||
packagesWithConfig.map(async (pkg) => {
|
||||
const config = (await getRefineConfig(pkg.path, true)) ??
|
||||
(await getRefineConfig(pkg.path, false)) ?? {
|
||||
swizzle: { items: [] },
|
||||
};
|
||||
return {
|
||||
...pkg,
|
||||
config,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectedPackage } = await inquirer.prompt<{
|
||||
selectedPackage: (typeof packageConfigs)[number];
|
||||
}>([
|
||||
{
|
||||
type: "autocomplete",
|
||||
pageSize: 10,
|
||||
name: "selectedPackage",
|
||||
message: "Which package do you want to swizzle?",
|
||||
emptyText: "No packages found.",
|
||||
source: getAutocompleteSource(
|
||||
packageConfigs
|
||||
.sort((a, b) =>
|
||||
(a.config?.group ?? "").localeCompare(b.config?.group ?? ""),
|
||||
)
|
||||
.map((pkg) => ({
|
||||
label: pkg.config?.name ?? pkg.name,
|
||||
value: pkg,
|
||||
group: pkg.config?.group,
|
||||
})),
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
const {
|
||||
swizzle: { items, transform },
|
||||
} = selectedPackage.config;
|
||||
|
||||
let selectedComponent: SwizzleFile | undefined = undefined;
|
||||
|
||||
if (items.length === 0) {
|
||||
console.log(
|
||||
`No swizzle items found for ${chalk.bold(
|
||||
selectedPackage.config?.name ?? selectedPackage.name,
|
||||
)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (items.length === 1) {
|
||||
selectedComponent = items[0];
|
||||
} else if (items.length > 1) {
|
||||
const response = await inquirer.prompt<{
|
||||
selectedComponent: SwizzleFile;
|
||||
}>([
|
||||
{
|
||||
type: "list",
|
||||
pageSize: 10,
|
||||
name: "selectedComponent",
|
||||
message: "Which component do you want to swizzle?",
|
||||
emptyText: "No components found.",
|
||||
choices: getAutocompleteSource(
|
||||
items.sort((a, b) => a.group.localeCompare(b.group)),
|
||||
)({}, ""),
|
||||
},
|
||||
]);
|
||||
selectedComponent = response.selectedComponent;
|
||||
}
|
||||
|
||||
if (!selectedComponent) {
|
||||
console.log(
|
||||
`No swizzle items selected for ${chalk.bold(
|
||||
selectedPackage.config?.name ?? selectedPackage.name,
|
||||
)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// this will be prepended to `destPath` values
|
||||
const projectPathPrefix = getPathPrefix();
|
||||
|
||||
const createdFiles = await Promise.all(
|
||||
selectedComponent.files.map(async (file) => {
|
||||
try {
|
||||
const srcPath = file.src
|
||||
? path.join(selectedPackage.path, file.src)
|
||||
: undefined;
|
||||
const destPath = file.dest
|
||||
? path.join(process.cwd(), projectPathPrefix, file.dest)
|
||||
: undefined;
|
||||
|
||||
if (!srcPath) {
|
||||
console.log("No src path found for file", file);
|
||||
return ["", SWIZZLE_CODES.SOURCE_PATH_NOT_FOUND] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
}
|
||||
|
||||
if (!destPath) {
|
||||
console.log("No destination path found for file", file);
|
||||
return ["", SWIZZLE_CODES.TARGET_PATH_NOT_FOUND] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
}
|
||||
|
||||
const hasSrc = await pathExists(srcPath);
|
||||
|
||||
if (!hasSrc) {
|
||||
return [destPath, SWIZZLE_CODES.SOURCE_PATH_NOT_A_FILE] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
}
|
||||
|
||||
const srcContent = await readFile(srcPath, "utf-8");
|
||||
const isDestExist = await pathExists(destPath);
|
||||
|
||||
if (isDestExist) {
|
||||
return [destPath, SWIZZLE_CODES.TARGET_ALREADY_EXISTS] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
}
|
||||
|
||||
await ensureFile(destPath);
|
||||
|
||||
const parsedContent = parseSwizzleBlocks(srcContent);
|
||||
|
||||
const fileTransformedContent =
|
||||
file.transform?.(parsedContent) ?? parsedContent;
|
||||
|
||||
const transformedContent =
|
||||
transform?.(fileTransformedContent, srcPath, destPath) ??
|
||||
fileTransformedContent;
|
||||
|
||||
const reorderedContent = reorderImports(transformedContent);
|
||||
|
||||
const formatted = await prettierFormat(reorderedContent);
|
||||
|
||||
await writeFile(destPath, formatted);
|
||||
|
||||
return [destPath, SWIZZLE_CODES.SUCCESS] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
} catch (error) {
|
||||
return ["", SWIZZLE_CODES.UNKNOWN_ERROR] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (createdFiles.length > 0) {
|
||||
printSwizzleMessage({
|
||||
files: createdFiles,
|
||||
label: selectedComponent.label,
|
||||
message: selectedComponent.message,
|
||||
});
|
||||
|
||||
if (selectedComponent?.requiredPackages?.length) {
|
||||
await installRequiredPackages(selectedComponent.requiredPackages);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default swizzle;
|
||||
@@ -0,0 +1,106 @@
|
||||
import inquirer from "inquirer";
|
||||
import { installPackages, getPreferedPM } from "@utils/package";
|
||||
|
||||
jest.mock("inquirer");
|
||||
jest.mock("@utils/package");
|
||||
|
||||
const getPreferedPMMock = getPreferedPM as jest.MockedFunction<
|
||||
typeof getPreferedPM
|
||||
>;
|
||||
const inquirerMock = inquirer as jest.Mocked<typeof inquirer>;
|
||||
const installPackagesMock = installPackages as jest.MockedFunction<
|
||||
typeof installPackages
|
||||
>;
|
||||
|
||||
import * as installRequiredPackages from "./index";
|
||||
|
||||
describe("should prompt for package installation and install packages if confirmed", () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should install required packages", async () => {
|
||||
inquirerMock.prompt.mockResolvedValueOnce({
|
||||
installRequiredPackages: true,
|
||||
});
|
||||
|
||||
const requiredPackages = ["react", "react-dom"];
|
||||
|
||||
const installRequiredPackagesSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"installRequiredPackages",
|
||||
);
|
||||
const promptForPackageInstallationSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"promptForPackageInstallation",
|
||||
);
|
||||
const displayManualInstallationCommandSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"displayManualInstallationCommand",
|
||||
);
|
||||
|
||||
await installRequiredPackages.installRequiredPackages(requiredPackages);
|
||||
|
||||
expect(installRequiredPackagesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(installRequiredPackagesSpy).toHaveBeenCalledWith(requiredPackages);
|
||||
|
||||
expect(promptForPackageInstallationSpy).toHaveBeenCalledTimes(1);
|
||||
expect(promptForPackageInstallationSpy).toHaveBeenCalledWith(
|
||||
requiredPackages,
|
||||
);
|
||||
|
||||
expect(displayManualInstallationCommandSpy).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(installPackagesMock).toHaveBeenCalledTimes(1);
|
||||
expect(installPackagesMock).toHaveBeenCalledWith(requiredPackages);
|
||||
});
|
||||
|
||||
it("should display manual installation command if not confirmed", async () => {
|
||||
inquirerMock.prompt.mockResolvedValueOnce({
|
||||
installRequiredPackages: false,
|
||||
});
|
||||
getPreferedPMMock.mockResolvedValueOnce({
|
||||
name: "npm",
|
||||
version: "1",
|
||||
});
|
||||
|
||||
const requiredPackages = ["react", "react-dom"];
|
||||
|
||||
const installRequiredPackagesSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"installRequiredPackages",
|
||||
);
|
||||
const promptForPackageInstallationSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"promptForPackageInstallation",
|
||||
);
|
||||
const displayManualInstallationCommandSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"displayManualInstallationCommand",
|
||||
);
|
||||
const consoleSpy = jest.spyOn(console, "log").mockImplementation();
|
||||
|
||||
await installRequiredPackages.installRequiredPackages(requiredPackages);
|
||||
|
||||
expect(installRequiredPackagesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(installRequiredPackagesSpy).toHaveBeenCalledWith(requiredPackages);
|
||||
|
||||
expect(promptForPackageInstallationSpy).toHaveBeenCalledTimes(1);
|
||||
expect(promptForPackageInstallationSpy).toHaveBeenCalledWith(
|
||||
requiredPackages,
|
||||
);
|
||||
|
||||
expect(displayManualInstallationCommandSpy).toHaveBeenCalledTimes(1);
|
||||
expect(displayManualInstallationCommandSpy).toHaveBeenCalledWith(
|
||||
requiredPackages,
|
||||
);
|
||||
expect(getPreferedPM).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(installPackagesMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"\nYou can install them manually by running this command:",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import inquirer from "inquirer";
|
||||
import chalk from "chalk";
|
||||
import { getPreferedPM, installPackages, pmCommands } from "@utils/package";
|
||||
|
||||
export const installRequiredPackages = async (requiredPackages: string[]) => {
|
||||
const installRequiredPackages =
|
||||
await promptForPackageInstallation(requiredPackages);
|
||||
|
||||
if (!installRequiredPackages) {
|
||||
await displayManualInstallationCommand(requiredPackages);
|
||||
} else {
|
||||
await installPackages(requiredPackages);
|
||||
}
|
||||
};
|
||||
|
||||
export const promptForPackageInstallation = async (
|
||||
requiredPackages: string[],
|
||||
) => {
|
||||
const message =
|
||||
"This component requires following packages to be installed:\n"
|
||||
.concat(requiredPackages.map((pkg) => ` - ${pkg}`).join("\n"))
|
||||
.concat("\nDo you want to install them?");
|
||||
|
||||
const { installRequiredPackages } = await inquirer.prompt<{
|
||||
installRequiredPackages: boolean;
|
||||
}>([
|
||||
{
|
||||
type: "confirm",
|
||||
name: "installRequiredPackages",
|
||||
default: true,
|
||||
message,
|
||||
},
|
||||
]);
|
||||
|
||||
return installRequiredPackages;
|
||||
};
|
||||
|
||||
export const displayManualInstallationCommand = async (
|
||||
requiredPackages: string[],
|
||||
) => {
|
||||
const pm = await getPreferedPM();
|
||||
const pmCommand = pmCommands[pm.name].add.join(" ");
|
||||
const packages = requiredPackages.join(" ");
|
||||
const command = `${pm.name} ${pmCommand} ${packages}`;
|
||||
|
||||
console.log("\nYou can install them manually by running this command:");
|
||||
console.log(chalk.bold.blueBright(command));
|
||||
};
|
||||
Reference in New Issue
Block a user