fork refine

This commit is contained in:
Stefan Pejcic
2024-02-05 10:23:04 +01:00
parent 3fffde9a8f
commit 8496a83edb
3634 changed files with 715528 additions and 2 deletions

View File

@@ -0,0 +1,97 @@
import { copySync, mkdirSync, pathExistsSync } from "fs-extra";
import { join } from "path";
import { getProjectType } from "@utils/project";
import { getProviderPath } from "@utils/resource";
const templatePath = `${__dirname}/../templates/provider`;
export type Provider =
| "auth"
| "live"
| "data"
| "access-control"
| "notification"
| "i18n"
| "audit-log";
export const providerArgs: Provider[] = [
"auth",
"live",
"data",
"access-control",
"notification",
"i18n",
"audit-log",
];
export const createProvider = (providers: string[], pathFromArgs?: string) => {
providers.forEach((arg) => {
const { fileName, templatePath } = getProviderOptions(arg as Provider);
const folderPath = pathFromArgs ?? getDefaultPath();
const filePath = join(folderPath, fileName);
const fullPath = join(process.cwd(), folderPath, fileName);
if (pathExistsSync(fullPath)) {
console.error(`❌ Provider (${filePath}) already exist!`);
return;
}
// create destination dir
mkdirSync(folderPath, { recursive: true });
// copy template file to destination
copySync(templatePath, fullPath);
console.log(`🎉 Provider (${filePath}) created successfully!`);
});
};
export const providerOptions: {
[key in Provider]: {
fileName: string;
templatePath: string;
};
} = {
auth: {
fileName: "auth-provider.tsx",
templatePath: `${templatePath}/demo-auth-provider.tsx`,
},
live: {
fileName: "live-provider.tsx",
templatePath: `${templatePath}/demo-live-provider.tsx`,
},
data: {
fileName: "data-provider.tsx",
templatePath: `${templatePath}/demo-data-provider.tsx`,
},
"access-control": {
fileName: "access-control-provider.tsx",
templatePath: `${templatePath}/demo-access-control-provider.tsx`,
},
notification: {
fileName: "notification-provider.tsx",
templatePath: `${templatePath}/demo-notification-provider.tsx`,
},
i18n: {
fileName: "i18n-provider.tsx",
templatePath: `${templatePath}/demo-i18n-provider.tsx`,
},
"audit-log": {
fileName: "audit-log-provider.tsx",
templatePath: `${templatePath}/demo-audit-log-provider.tsx`,
},
};
export const getProviderOptions = (provider: Provider) => {
if (!providerOptions?.[provider]) {
throw new Error(`Invalid provider: ${provider}`);
}
return providerOptions[provider];
};
export const getDefaultPath = () => {
const projectType = getProjectType();
const { path } = getProviderPath(projectType);
return path;
};

View File

@@ -0,0 +1,165 @@
import {
copySync,
unlinkSync,
moveSync,
pathExistsSync,
mkdirSync,
} from "fs-extra";
import temp from "temp";
import { plural } from "pluralize";
import execa from "execa";
import inquirer from "inquirer";
import { join } from "path";
import { compileDir } from "@utils/compile";
import { uppercaseFirstChar } from "@utils/text";
import { getProjectType, getUIFramework } from "@utils/project";
import { getResourcePath } from "@utils/resource";
const defaultActions = ["list", "create", "edit", "show"];
export const createResources = async (
params: { actions?: string; path?: string },
resources: string[],
) => {
const destinationPath =
params?.path || getResourcePath(getProjectType()).path;
let actions = params.actions || defaultActions.join(",");
if (!resources.length) {
// TODO: Show inquirer
const { name, selectedActions } = await inquirer.prompt<{
name: string;
selectedActions: string[];
}>([
{
type: "input",
name: "name",
message: "Resource Name",
validate: (value) => {
if (!value) {
return "Resource Name is required";
}
return true;
},
},
{
type: "checkbox",
name: "selectedActions",
message: "Select Actions",
choices: defaultActions,
default: params?.actions?.split(","),
},
]);
resources = [name];
actions = selectedActions.join(",");
}
resources.forEach((resourceName) => {
const customActions = actions ? actions.split(",") : undefined;
const resourceFolderName = plural(resourceName).toLowerCase();
// check exist resource
const resourcePath = join(
process.cwd(),
destinationPath,
resourceFolderName,
);
if (pathExistsSync(resourcePath)) {
console.error(
`❌ Resource (${join(
destinationPath,
resourceFolderName,
)}) already exist!`,
);
return;
}
// uppercase first letter
const resource = uppercaseFirstChar(resourceName);
// get the project type
const uiFramework = getUIFramework();
const sourceDir = `${__dirname}/../templates/resource`;
// create temp dir
const tempDir = generateTempDir();
// copy template files
copySync(sourceDir, tempDir);
const compileParams = {
resourceName,
resource,
actions: customActions || defaultActions,
uiFramework,
};
// compile dir
compileDir(tempDir, compileParams);
// delete ignored actions
if (customActions) {
defaultActions.forEach((action) => {
if (!customActions.includes(action)) {
unlinkSync(`${tempDir}/${action}.tsx`);
}
});
}
// create desctination dir
mkdirSync(destinationPath, { recursive: true });
// copy to destination
const destinationResourcePath = `${destinationPath}/${resourceFolderName}`;
let moveSyncOptions = {};
// empty dir override
if (pathExistsSync(destinationResourcePath)) {
moveSyncOptions = { overwrite: true };
}
moveSync(tempDir, destinationResourcePath, moveSyncOptions);
// clear temp dir
temp.cleanupSync();
const jscodeshiftExecutable = require.resolve(".bin/jscodeshift");
const { stderr, stdout } = execa.sync(jscodeshiftExecutable, [
"./",
"--extensions=ts,tsx,js,jsx",
"--parser=tsx",
`--transform=${__dirname}/../src/transformers/resource.ts`,
`--ignore-pattern=**/.cache/**`,
`--ignore-pattern=**/node_modules/**`,
`--ignore-pattern=**/build/**`,
`--ignore-pattern=**/.next/**`,
// pass custom params to transformer file
`--__actions=${compileParams.actions}`,
`--__pathAlias=${getResourcePath(getProjectType()).alias}`,
`--__resourceFolderName=${resourceFolderName}`,
`--__resource=${resource}`,
`--__resourceName=${resourceName}`,
]);
// console.log(stdout);
if (stderr) {
console.log(stderr);
}
console.log(
`🎉 Resource (${destinationResourcePath}) generated successfully!`,
);
});
return;
};
const generateTempDir = (): string => {
temp.track();
return temp.mkdirSync("resource");
};

View File

@@ -0,0 +1,70 @@
import * as utilsProject from "../../utils/project/index";
import * as utilsResource from "../../utils/resource/index";
import { getGroupedArgs } from ".";
import {
getDefaultPath,
getProviderOptions,
providerArgs,
providerOptions,
} from "./create-provider";
import { ProjectTypes } from "@definitions/projectTypes";
describe("add", () => {
it.each([
{
args: ["auth", "live", "resource", "user", "post", "data"],
expected: {
providers: ["auth", "live"],
resources: ["user", "post", "data"],
},
},
{
args: ["resource", "auth", "live", "user", "post", "data"],
expected: {
providers: [],
resources: ["auth", "live", "user", "post", "data"],
},
},
{
args: ["auth", "live", "data", "resource"],
expected: {
providers: ["auth", "live", "data"],
resources: [],
},
},
{
args: ["auth", "live", "data", "not-provider"],
expected: {
providers: ["auth", "live", "data"],
resources: [],
},
},
])("should group by args", ({ args, expected }) => {
const { providers, resources } = getGroupedArgs(args);
expect(providers).toEqual(expected.providers);
expect(resources).toEqual(expected.resources);
});
it.each(providerArgs)(
"should return proivder options for %s",
(provider) => {
const option = getProviderOptions(provider);
expect(option).toEqual(providerOptions[provider]);
},
);
it("should get default provider path for provider", () => {
jest.spyOn(utilsProject, "getProjectType").mockReturnValue(
ProjectTypes.VITE,
);
jest.spyOn(utilsResource, "getProviderPath").mockReturnValue({
alias: "test-alias",
path: "test-path",
});
const path = getDefaultPath();
expect(path).toEqual("test-path");
});
});

View File

@@ -0,0 +1,98 @@
import { Argument, Command } from "commander";
import { Provider, createProvider, providerArgs } from "./create-provider";
import { createResources } from "./create-resource";
import { getPreferedPM } from "@utils/package";
const load = (program: Command) => {
return program
.command("add")
.allowExcessArguments(false)
.addArgument(
new Argument("[provider]", "Create a new provider")
.choices([...providerArgs, "resource"])
.argOptional(),
)
.addArgument(
new Argument(
"[resource...]",
"Create a new resource files",
).argOptional(),
)
.option("-p, --path [path]", "Path to generate files")
.option(
"-a, --actions [actions]",
"Only generate the specified resource actions. (ex: list,create,edit,show)",
"list,create,edit,show",
)
.action(action);
};
const action = async (
_provider: string,
_resource: string,
options: { actions: string; path?: string },
command: Command,
) => {
const args = command?.args;
if (!args.length) {
await printNoArgs();
return;
}
const { providers, resources } = getGroupedArgs(args);
if (providers.length) {
createProvider(providers, options?.path);
}
if (args.includes("resource")) {
createResources(
{
actions: options?.actions,
path: options?.path,
},
resources,
);
}
};
// we need to group args.
// for example: add auth live resource user post data
// should be grouped like this:
// providers: [add, auth, live, data]. resource: [user, post]
export const getGroupedArgs = (args: string[]) => {
const resourceIndex = args.findIndex((arg) => arg === "resource");
if (resourceIndex === -1)
return {
providers: getValidProviders(args as Provider[]),
resources: [],
};
const providers = getValidProviders(
args.slice(0, resourceIndex) as Provider[],
);
const resources = args.slice(resourceIndex + 1);
return { providers, resources };
};
const printNoArgs = async () => {
const { name } = await getPreferedPM();
console.log("❌ Please provide a feature name");
console.log(
`For more information please use: "${name} run refine add help"`,
);
};
const getValidProviders = (providers: Provider[]) => {
return providers.filter((provider) => {
if (providerArgs.includes(provider)) return true;
console.log(`❌ "${provider}" is not a valid provider`);
return false;
});
};
export default load;

View File

@@ -0,0 +1,66 @@
import {
NpmOutdatedResponse,
RefinePackageInstalledVersionData,
} from "@definitions/package";
import * as checkUpdates from "./index";
const { getOutdatedRefinePackages } = checkUpdates;
test("Get outdated refine packages", async () => {
const testCases: {
input: NpmOutdatedResponse;
output: RefinePackageInstalledVersionData[];
}[] = [
{
input: {
"@refinedev/core": {
current: "1.0.0",
wanted: "1.0.1",
latest: "2.0.0",
},
"@refinedev/cli": {
current: "1.1.1",
wanted: "1.1.1",
latest: "1.1.0",
},
"@pankod/canvas2video": {
current: "1.1.1",
wanted: "1.1.1",
latest: "1.1.1",
},
"@owner/package-name": {
current: "1.1.1",
wanted: "1.1.1",
latest: "1.1.0",
},
"@owner/package-name1": {
current: "N/A",
wanted: "undefined",
latest: "NaN",
},
"@owner/refine-react": {
current: "1.0.0",
wanted: "1.0.1",
latest: "2.0.0",
},
},
output: [
{
name: "@refinedev/core",
current: "1.0.0",
wanted: "1.0.1",
latest: "2.0.0",
changelog: "https://c.refine.dev/core",
},
],
},
];
for (const testCase of testCases) {
jest.spyOn(checkUpdates, "getOutdatedPackageList").mockResolvedValue(
testCase.input,
);
const result = await getOutdatedRefinePackages();
expect(result).toEqual(testCase.output);
}
});

View File

@@ -0,0 +1,95 @@
import { Command } from "commander";
import { printUpdateWarningTable } from "@components/update-warning-table";
import { pmCommands } from "@utils/package";
import execa from "execa";
import spinner from "@utils/spinner";
import {
NpmOutdatedResponse,
RefinePackageInstalledVersionData,
} from "@definitions/package";
import semverDiff from "semver-diff";
const load = (program: Command) => {
return program
.command("check-updates")
.description("Check all installed `refine` packages are up to date")
.action(action);
};
const action = async () => {
const packages = await spinner(isRefineUptoDate, "Checking for updates...");
if (!packages.length) {
console.log("All `refine` packages are up to date 🎉\n");
return;
}
await printUpdateWarningTable({ data: packages });
};
/**
*
* @returns `refine` packages that have updates.
* @returns `[]` if no refine package found.
* @returns `[]` if all `refine` packages are up to date.
*/
export const isRefineUptoDate = async () => {
const refinePackages = await getOutdatedRefinePackages();
return refinePackages;
};
export const getOutdatedRefinePackages = async () => {
const packages = await getOutdatedPackageList();
if (!packages) return [];
const list: RefinePackageInstalledVersionData[] = [];
let changelog: string | undefined = undefined;
Object.keys(packages).forEach((packageName) => {
const dependency = packages[packageName];
if (packageName.includes("@refinedev")) {
changelog = packageName.replace(
/@refinedev\//,
"https://c.refine.dev/",
);
list.push({
name: packageName,
current: dependency.current,
wanted: dependency.wanted,
latest: dependency.latest,
changelog,
});
}
});
// When user has installed `next` version, it will be ahead of the `latest` version. But `npm outdated` command still returns as an outdated package.
// So we need to filter out the if `current` version is ahead of `latest` version.
// ex: in the npm registry `next` version is 1.1.1 -> [current:1.1.1, wanted:1.1.1, latest:1.1.0]
const filteredList = list.filter((item) => {
const diff = semverDiff(item.current, item.latest);
return !!diff;
});
return filteredList;
};
export const getOutdatedPackageList = async () => {
const pm = "npm";
const { stdout, timedOut } = await execa(pm, pmCommands[pm].outdatedJson, {
reject: false,
timeout: 25 * 1000,
});
if (timedOut) {
throw new Error("Timed out while checking for updates.");
}
if (!stdout) return null;
return JSON.parse(stdout) as NpmOutdatedResponse | null;
};
export default load;

View File

@@ -0,0 +1,36 @@
import { Command } from "commander";
import { getProjectType } from "@utils/project";
import { getResourcePath } from "@utils/resource";
import { createResources } from "@commands/add/create-resource";
const load = (program: Command) => {
const projectType = getProjectType();
const { path } = getResourcePath(projectType);
return program
.command("create-resource")
.allowExcessArguments(true)
.description(
`Create a new resource files (deprecated, please use "add resource" command)`,
)
.option(
"-a, --actions [actions]",
"Only generate the specified actions. (ex: list,create,edit,show)",
"list,create,edit,show",
)
.option(
"-p, --path [path]",
"Path to generate the resource files",
path,
)
.action(action);
};
const action = async (
params: { actions: string; path: string },
options: Command,
) => {
createResources(params, options.args);
};
export default load;

View File

@@ -0,0 +1,225 @@
import { server } from "@refinedev/devtools-server";
import { addDevtoolsComponent } from "@transformers/add-devtools-component";
import {
getInstalledRefinePackagesFromNodeModules,
getPackageJson,
getPreferedPM,
installPackagesSync,
isDevtoolsInstalled,
} from "@utils/package";
import { hasDefaultScript } from "@utils/refine";
import spinner from "@utils/spinner";
import boxen from "boxen";
import cardinal from "cardinal";
import chalk from "chalk";
import { Argument, Command } from "commander";
import dedent from "dedent";
import semver from "semver";
type DevtoolsCommand = "start" | "init";
const commands: DevtoolsCommand[] = ["start", "init"];
const defaultCommand: DevtoolsCommand = "start";
const minRefineCoreVersionForDevtools = "4.42.0";
const load = (program: Command) => {
return program
.command("devtools")
.description(
"Start or install refine's devtools server; it starts on port 5001.",
)
.addArgument(
new Argument("[command]", "devtools related commands")
.choices(commands)
.default(defaultCommand),
)
.addHelpText(
"after",
`
Commands:
start Start refine's devtools server
init Install refine's devtools client and adds it to your project
`,
)
.action(action);
};
export const action = async (command: DevtoolsCommand) => {
switch (command) {
case "start":
devtoolsRunner();
return;
case "init":
devtoolsInstaller();
return;
}
};
const devtoolsInstaller = async () => {
const corePackage = await getRefineCorePackage();
const isInstalled = await spinner(
isDevtoolsInstalled,
"Checking if devtools is installed...",
);
if (isInstalled) {
console.log("🎉 refine devtools is already installed");
return;
}
if (
corePackage &&
(await validateCorePackageIsNotDeprecated({ pkg: corePackage }))
) {
return;
}
console.log("🌱 Installing refine devtools...");
const packagesToInstall = ["@refinedev/devtools@latest"];
// we should update core package if it is lower than minRefineCoreVersionForDevtools
if (
!corePackage ||
semver.lt(corePackage.version, minRefineCoreVersionForDevtools)
) {
packagesToInstall.push("@refinedev/core@latest");
console.log(`🌱 refine core package is being updated for devtools...`);
}
await installPackagesSync(packagesToInstall);
// empty line
console.log("");
console.log("");
console.log("🌱 Adding devtools component to your project....");
await addDevtoolsComponent();
console.log(
"✅ refine devtools package and components added to your project",
);
// if core package is updated, we should show the updated version
if (packagesToInstall.includes("@refinedev/core@latest")) {
const updatedCorePackage = await getRefineCorePackage();
console.log(
`✅ refine core package updated from ${
corePackage?.version ?? "unknown"
} to ${updatedCorePackage?.version ?? "unknown"}`,
);
console.log(
` Changelog: ${chalk.underline.blueBright(
`https://c.refine.dev/core#${
corePackage?.version.replaceAll(".", "") ?? ""
}`,
)}`,
);
}
// empty line
console.log("");
const { dev } = hasDefaultScript();
if (dev) {
console.log(
`🙌 You're good to go. "npm run dev" will automatically starts the devtools server.`,
);
console.log(
`👉 You can also start the devtools server manually by running "refine devtools start"`,
);
return;
}
if (!dev) {
const scriptDev = getPackageJson().scripts?.dev;
console.log(
`🚨 Your have modified the "dev" script in your package.json. Because of that, "npm run dev" will not start the devtools server automatically.`,
);
console.log(`👉 You can append "refine devtools" to "dev" script`);
console.log(
`👉 You can start the devtools server manually by running "refine devtools"`,
);
// empty line
console.log("");
console.log(
boxen(
cardinal.highlight(
dedent(`
{
"scripts": {
"dev": "${scriptDev} & refine devtools",
"refine": "refine"
}
}
`),
),
{
padding: 1,
title: "package.json",
dimBorder: true,
borderColor: "blueBright",
borderStyle: "round",
},
),
);
}
};
export const devtoolsRunner = async () => {
const corePackage = await getRefineCorePackage();
if (corePackage) {
if (await validateCorePackageIsNotDeprecated({ pkg: corePackage })) {
return;
}
if (semver.lt(corePackage.version, minRefineCoreVersionForDevtools)) {
console.log(
`🚨 You're using an old version of refine(${corePackage.version}). refine version should be @4.42.0 or higher to use devtools.`,
);
const pm = await getPreferedPM();
console.log(
`👉 You can update @refinedev/core package by running "${pm.name} run refine update"`,
);
return;
}
}
server();
};
const getRefineCorePackage = async () => {
const installedRefinePackages =
await getInstalledRefinePackagesFromNodeModules();
const corePackage = installedRefinePackages?.find(
(pkg) =>
pkg.name === "@refinedev/core" ||
pkg.name === "@pankod/refine-core",
);
if (!corePackage) {
return undefined;
}
return corePackage;
};
export const validateCorePackageIsNotDeprecated = async ({
pkg,
}: {
pkg: { name: string; version: string };
}) => {
if (pkg.name === "@pankod/refine-core" || semver.lt(pkg.version, "4.0.0")) {
console.log(
`🚨 You're using an old version of refine(${pkg.version}). refine version should be @4.42.0 or higher to use devtools.`,
);
console.log("You can follow migration guide to update refine.");
console.log(
chalk.blue("https://refine.dev/docs/migration-guide/3x-to-4x/"),
);
return true;
}
return false;
};
export default load;

View File

@@ -0,0 +1,151 @@
import { ENV } from "@utils/env";
import { Command } from "commander";
import express from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
import { OnProxyResCallback } from "http-proxy-middleware/dist/types";
const load = (program: Command) => {
return program
.command("proxy")
.description("Manage proxy settings")
.action(action)
.option(
"-p, --port [port]",
"Port to serve the proxy server. You can also set this with the `REFINE_PROXY_PORT` environment variable.",
ENV.REFINE_PROXY_PORT,
)
.option(
"-t, --target [target]",
"Target to proxy. You can also set this with the `REFINE_PROXY_TARGET` environment variable.",
ENV.REFINE_PROXY_TARGET,
)
.option(
"-d, --domain [domain]",
"Domain to proxy. You can also set this with the `REFINE_PROXY_DOMAIN` environment variable.",
ENV.REFINE_PROXY_DOMAIN,
)
.option(
"-r, --rewrite-url [rewrite URL]",
"Rewrite URL for redirects. You can also set this with the `REFINE_PROXY_REWRITE_URL` environment variable.",
ENV.REFINE_PROXY_REWRITE_URL,
);
};
const action = async ({
port,
target,
domain,
rewriteUrl,
}: {
port: string;
target: string;
domain: string;
rewriteUrl: string;
}) => {
const app = express();
const targetUrl = new URL(target);
const onProxyRes: OnProxyResCallback | undefined =
targetUrl.protocol === "http:"
? (proxyRes) => {
if (proxyRes.headers["set-cookie"]) {
proxyRes.headers["set-cookie"]?.forEach((cookie, i) => {
if (
proxyRes &&
proxyRes.headers &&
proxyRes.headers["set-cookie"]
) {
proxyRes.headers["set-cookie"][i] =
cookie.replace("Secure;", "");
}
});
}
}
: undefined;
app.use(
"/.refine",
createProxyMiddleware({
target: `${domain}/.refine`,
changeOrigin: true,
pathRewrite: { "^/.refine": "" },
logProvider: () => ({
log: console.log,
info: (msg) => {
if (`${msg}`.includes("Proxy rewrite rule created")) return;
if (`${msg}`.includes("Proxy created")) {
console.log(
`Proxying localhost:${port}/.refine to ${domain}/.refine`,
);
} else if (msg) {
console.log(msg);
}
},
warn: console.warn,
debug: console.debug,
error: console.error,
}),
}),
);
app.use(
"/.auth",
createProxyMiddleware({
target: `${domain}/.auth`,
changeOrigin: true,
cookieDomainRewrite: {
"refine.dev": "",
},
headers: {
"auth-base-url-rewrite": `${rewriteUrl}/.auth`,
},
pathRewrite: { "^/.auth": "" },
logProvider: () => ({
log: console.log,
info: (msg) => {
if (`${msg}`.includes("Proxy rewrite rule created")) return;
if (`${msg}`.includes("Proxy created")) {
console.log(
`Proxying localhost:${port}/.auth to ${domain}/.auth`,
);
} else if (msg) {
console.log(msg);
}
},
warn: console.warn,
debug: console.debug,
error: console.error,
}),
onProxyRes,
}),
);
app.use(
"*",
createProxyMiddleware({
target: `${target}`,
changeOrigin: true,
ws: true,
logProvider: () => ({
log: console.log,
info: (msg) => {
if (`${msg}`.includes("Proxy created")) {
console.log(`Proxying localhost:${port} to ${target}`);
} else if (msg) {
console.log(msg);
}
},
warn: console.warn,
debug: console.debug,
error: console.error,
}),
}),
);
app.listen(Number(port));
};
export default load;

View File

@@ -0,0 +1,47 @@
import { Command, Option } from "commander";
import { getProjectType } from "@utils/project";
import { projectScripts } from "../projectScripts";
import { runScript } from "../runScript";
import { updateNotifier } from "src/update-notifier";
import { getPlatformOptionDescription, getRunnerDescription } from "../utils";
import { ProjectTypes } from "@definitions/projectTypes";
const build = (program: Command) => {
return program
.command("build")
.description(getRunnerDescription("build"))
.allowUnknownOption(true)
.addOption(
new Option(
"-p, --platform <platform>",
getPlatformOptionDescription(),
).choices(
Object.values(ProjectTypes).filter(
(type) => type !== ProjectTypes.UNKNOWN,
),
),
)
.argument("[args...]")
.action(action);
};
const action = async (
args: string[],
{ platform }: { platform: ProjectTypes },
) => {
const projectType = getProjectType(platform);
const binPath = projectScripts[projectType].getBin("build");
const command = projectScripts[projectType].getBuild(args);
await updateNotifier();
try {
await runScript(binPath, command);
} catch (error) {
process.exit(1);
}
};
export default build;

View File

@@ -0,0 +1,58 @@
import { ProjectTypes } from "@definitions/projectTypes";
import { getProjectType } from "@utils/project";
import { Command, Option } from "commander";
import { updateNotifier } from "src/update-notifier";
import { devtoolsRunner } from "src/commands/devtools";
import { projectScripts } from "../projectScripts";
import { runScript } from "../runScript";
import { getPlatformOptionDescription, getRunnerDescription } from "../utils";
import { isDevtoolsInstalled } from "@utils/package";
const dev = (program: Command) => {
return program
.command("dev")
.description(getRunnerDescription("dev"))
.allowUnknownOption(true)
.addOption(
new Option(
"-p, --platform <platform>",
getPlatformOptionDescription(),
).choices(
Object.values(ProjectTypes).filter(
(type) => type !== ProjectTypes.UNKNOWN,
),
),
)
.addOption(
new Option(
"-d, --devtools <devtools>",
"Start refine's devtools server",
).default("true", "true if devtools is installed"),
)
.argument("[args...]")
.action(action);
};
const action = async (
args: string[],
{ platform, ...params }: { devtools: string; platform: ProjectTypes },
) => {
const projectType = getProjectType(platform);
const binPath = projectScripts[projectType].getBin("dev");
const command = projectScripts[projectType].getDev(args);
await updateNotifier();
const devtoolsDefault = await isDevtoolsInstalled();
const devtools = params.devtools === "false" ? false : devtoolsDefault;
if (devtools) {
devtoolsRunner();
}
runScript(binPath, command);
};
export default dev;

View File

@@ -0,0 +1,6 @@
import start from "./start";
import dev from "./dev";
import build from "./build";
import run from "./run";
export { dev, start, build, run };

View File

@@ -0,0 +1,366 @@
import { projectScripts } from "./projectScripts";
import { ProjectTypes } from "@definitions/projectTypes";
describe("REACT_SCRIPT project type", () => {
const projectType = ProjectTypes.REACT_SCRIPT;
describe("getDev with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getDev([])).toEqual(["start"]);
});
});
describe("getStart with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getStart([])).toEqual(["start"]);
});
});
describe("getBuild with empty args", () => {
test('should return array with only "build" if args is empty', () => {
expect(projectScripts[projectType].getBuild([])).toEqual(["build"]);
});
});
describe("getDev", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getDev(args)).toEqual([
"start",
...args,
]);
});
});
describe("getStart", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getStart(args)).toEqual([
"start",
...args,
]);
});
});
describe("getBuild", () => {
test('should prepend "build" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getBuild(args)).toEqual([
"build",
...args,
]);
});
});
});
describe("Vite project type", () => {
const projectType = ProjectTypes.VITE;
describe("getDev with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getDev([])).toEqual(["dev"]);
});
});
describe("getStart with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getStart([])).toEqual([
"preview",
]);
});
});
describe("getBuild with empty args", () => {
test('should return array with only "build" if args is empty', () => {
expect(projectScripts[projectType].getBuild([])).toEqual(["build"]);
});
});
describe("getDev", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getDev(args)).toEqual([
"dev",
...args,
]);
});
});
describe("getStart", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getStart(args)).toEqual([
"preview",
...args,
]);
});
});
describe("getBuild", () => {
test('should prepend "build" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getBuild(args)).toEqual([
"build",
...args,
]);
});
});
});
describe("Next.js project type", () => {
const projectType = ProjectTypes.NEXTJS;
describe("getDev with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getDev([])).toEqual(["dev"]);
});
});
describe("getStart with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getStart([])).toEqual(["start"]);
});
});
describe("getBuild with empty args", () => {
test('should return array with only "build" if args is empty', () => {
expect(projectScripts[projectType].getBuild([])).toEqual(["build"]);
});
});
describe("getDev", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getDev(args)).toEqual([
"dev",
...args,
]);
});
});
describe("getStart", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getStart(args)).toEqual([
"start",
...args,
]);
});
});
describe("getBuild", () => {
test('should prepend "build" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getBuild(args)).toEqual([
"build",
...args,
]);
});
});
});
describe("Remix project type", () => {
const projectType = ProjectTypes.REMIX;
describe("getDev with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getDev([])).toEqual(["dev"]);
});
});
describe("getStart with empty args", () => {
test("should return default", () => {
const logSpy = jest.spyOn(console, "warn");
expect(projectScripts[projectType].getStart([])).toEqual([
"./build/index.js",
]);
expect(logSpy).toHaveBeenCalled();
});
});
describe("getBuild with empty args", () => {
test('should return array with only "build" if args is empty', () => {
expect(projectScripts[projectType].getBuild([])).toEqual(["build"]);
});
});
describe("getDev", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getDev(args)).toEqual([
"dev",
...args,
]);
});
});
describe("getStart", () => {
test("should return args", () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getStart(args)).toEqual([
...args,
]);
});
});
describe("getBuild", () => {
test('should prepend "build" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getBuild(args)).toEqual([
"build",
...args,
]);
});
});
});
describe("CRACO project type", () => {
const projectType = ProjectTypes.CRACO;
describe("getDev with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getDev([])).toEqual(["start"]);
});
});
describe("getStart with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getStart([])).toEqual(["start"]);
});
});
describe("getBuild with empty args", () => {
test('should return array with only "build" if args is empty', () => {
expect(projectScripts[projectType].getBuild([])).toEqual(["build"]);
});
});
describe("getDev", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getDev(args)).toEqual([
"start",
...args,
]);
});
});
describe("getStart", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getStart(args)).toEqual([
"start",
...args,
]);
});
});
describe("getBuild", () => {
test('should prepend "build" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getBuild(args)).toEqual([
"build",
...args,
]);
});
});
});
describe("PARCEL project type", () => {
const projectType = ProjectTypes.PARCEL;
describe("getDev with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getDev([])).toEqual(["start"]);
});
});
describe("getStart with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getStart([])).toEqual(["start"]);
});
});
describe("getBuild with empty args", () => {
test('should return array with only "build" if args is empty', () => {
expect(projectScripts[projectType].getBuild([])).toEqual(["build"]);
});
});
describe("getDev", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getDev(args)).toEqual([
"start",
...args,
]);
});
});
describe("getStart", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getStart(args)).toEqual([
"start",
...args,
]);
});
});
describe("getBuild", () => {
test('should prepend "build" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getBuild(args)).toEqual([
"build",
...args,
]);
});
});
});
describe("UNKNOWN project type", () => {
const projectType = ProjectTypes.UNKNOWN;
describe("getDev with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getDev([])).toEqual([]);
});
});
describe("getStart with empty args", () => {
test('should return array with only "start" if args is empty', () => {
expect(projectScripts[projectType].getStart([])).toEqual([]);
});
});
describe("getBuild with empty args", () => {
test('should return array with only "build" if args is empty', () => {
expect(projectScripts[projectType].getBuild([])).toEqual([]);
});
});
describe("getDev", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getDev(args)).toEqual([...args]);
});
});
describe("getStart", () => {
test('should prepend "start" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getStart(args)).toEqual([
...args,
]);
});
});
describe("getBuild", () => {
test('should prepend "build" to the args array', () => {
const args = ["--arg1", "--arg2"];
expect(projectScripts[projectType].getBuild(args)).toEqual([
...args,
]);
});
});
});

View File

@@ -0,0 +1,75 @@
import { ProjectTypes } from "@definitions/projectTypes";
/**
* Map `refine` cli commands to project script
*/
export const projectScripts = {
[ProjectTypes.REACT_SCRIPT]: {
getDev: (args: string[]) => ["start", ...args],
getStart: (args: string[]) => ["start", ...args],
getBuild: (args: string[]) => ["build", ...args],
getBin: () => require.resolve(".bin/react-scripts"),
},
[ProjectTypes.VITE]: {
getDev: (args: string[]) => ["dev", ...args],
getStart: (args: string[]) => ["preview", ...args],
getBuild: (args: string[]) => ["build", ...args],
getBin: () => require.resolve(".bin/vite"),
},
[ProjectTypes.NEXTJS]: {
getDev: (args: string[]) => ["dev", ...args],
getStart: (args: string[]) => ["start", ...args],
getBuild: (args: string[]) => ["build", ...args],
getBin: () => require.resolve(".bin/next"),
},
[ProjectTypes.REMIX]: {
getDev: (args: string[]) => ["dev", ...args],
getStart: (args: string[]) => {
// remix-serve accepts a path to the entry file as an argument
// if we have arguments, we will pass them to remix-serve and do nothing.
// ex: `refine start ./build/index.js`
const hasArgs = args?.length;
if (hasArgs) {
return args;
}
// otherwise print a warning and use `./build/index.js` as default
console.log();
console.warn(
`🚨 Remix requires a path to the entry file. Please provide it as an argument to \`refine start\` command in package.json scripts`,
);
console.warn("Refine will use `./build/index.js` as default");
console.warn("Example: `refine start ./build/index.js`");
console.log();
return ["./build/index.js"];
},
getBuild: (args: string[]) => ["build", ...args],
getBin: (type?: "dev" | "start" | "build") => {
const binName = type === "start" ? "remix-serve" : "remix";
return require.resolve(
`${process.cwd()}/node_modules/.bin/${binName}`,
);
},
},
[ProjectTypes.CRACO]: {
getDev: (args: string[]) => ["start", ...args],
getStart: (args: string[]) => ["start", ...args],
getBuild: (args: string[]) => ["build", ...args],
getBin: () => require.resolve(".bin/craco"),
},
[ProjectTypes.PARCEL]: {
getDev: (args: string[]) => ["start", ...args],
getStart: (args: string[]) => ["start", ...args],
getBuild: (args: string[]) => ["build", ...args],
getBin: () => require.resolve(".bin/parcel"),
},
[ProjectTypes.UNKNOWN]: {
getDev: (args: string[]) => [...args],
getStart: (args: string[]) => [...args],
getBuild: (args: string[]) => [...args],
getBin: () => {
return "unknown";
},
},
};

View File

@@ -0,0 +1,47 @@
import { getPreferedPM, getScripts } from "@utils/package";
import chalk from "chalk";
import { Command } from "commander";
import { runScript } from "../runScript";
const run = (program: Command) => {
return program
.command("run")
.description(
"Runs a defined package script. If no `command` is provided, it will list the available scripts",
)
.allowUnknownOption(true)
.argument("[command] [args...]")
.action(action);
};
const action = async (args: string[]) => {
const [script, ...restArgs] = args;
const scriptsInPackage = getScripts();
// Show available scripts when no script is provided
if (!script) {
console.log(`Available via ${chalk.blue("`refine run`")}:\n`);
for (const [key, value] of Object.entries(scriptsInPackage)) {
console.log(` ${key}`);
console.log(` ${chalk.dim(value)}`);
console.log();
}
return;
}
// Check if script exists in package.json
const isDefinedScript = Object.keys(scriptsInPackage).includes(script);
// If script is not defined, run from node_modules
if (!isDefinedScript) {
const binPath = `${process.cwd()}/node_modules/.bin/${script}`;
runScript(binPath, restArgs);
return;
}
const pm = await getPreferedPM();
runScript(pm.name, ["run", script, ...restArgs]);
};
export default run;

View File

@@ -0,0 +1,30 @@
import { ProjectTypes } from "@definitions/projectTypes";
import execa from "execa";
export const runScript = async (binPath: string, args: string[]) => {
if (binPath === "unknown") {
const supportedProjectTypes = Object.values(ProjectTypes)
.filter((v) => v !== "unknown")
.join(", ");
console.error(
`We couldn't find executable for your project. Supported executables are ${supportedProjectTypes}.\nPlease use your own script directly. If you think this is an issue, please report it at: https://github.com/refinedev/refine/issues`,
);
return;
}
const execution = execa(binPath, args, {
stdio: "pipe",
windowsHide: false,
env: {
FORCE_COLOR: "true",
...process.env,
},
});
execution.stdout?.pipe(process.stdout);
execution.stderr?.pipe(process.stderr);
return await execution;
};

View File

@@ -0,0 +1,42 @@
import { ProjectTypes } from "@definitions/projectTypes";
import { getProjectType } from "@utils/project";
import { Command, Option } from "commander";
import { updateNotifier } from "src/update-notifier";
import { projectScripts } from "../projectScripts";
import { runScript } from "../runScript";
import { getPlatformOptionDescription, getRunnerDescription } from "../utils";
const start = (program: Command) => {
return program
.command("start")
.description(getRunnerDescription("start"))
.allowUnknownOption(true)
.addOption(
new Option(
"-p, --platform <platform>",
getPlatformOptionDescription(),
).choices(
Object.values(ProjectTypes).filter(
(type) => type !== ProjectTypes.UNKNOWN,
),
),
)
.argument("[args...]")
.action(action);
};
const action = async (
args: string[],
{ platform }: { platform: ProjectTypes },
) => {
const projectType = getProjectType(platform);
const binPath = projectScripts[projectType].getBin("start");
const command = projectScripts[projectType].getStart(args);
await updateNotifier();
runScript(binPath, command);
};
export default start;

View File

@@ -0,0 +1,34 @@
import { ProjectTypes } from "@definitions/projectTypes";
import { getProjectType } from "@utils/project";
import { projectScripts } from "../projectScripts";
export const getRunnerDescription = (runner: "dev" | "start" | "build") => {
let projectType = getProjectType();
let command: string[] = [];
switch (runner) {
case "dev":
command = projectScripts[projectType].getDev([""]);
break;
case "start":
command = projectScripts[projectType].getStart([""]);
break;
case "build":
command = projectScripts[projectType].getBuild([""]);
break;
}
if (projectType === ProjectTypes.REMIX && runner === "start") {
projectType = "remix-serve" as ProjectTypes;
}
return `It runs: \`${projectType} ${command.join(
" ",
)}\`. Also accepts all the arguments \`${projectType}\` accepts.`;
};
export const getPlatformOptionDescription = () => {
return `Platform to run command on. \nex: ${Object.values(
ProjectTypes,
).join(", ")}`;
};

View File

@@ -0,0 +1,282 @@
import React from "react";
import path from "path";
import chalk from "chalk";
import inquirer from "inquirer";
import { 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 { 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;
} else 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;

View File

@@ -0,0 +1,110 @@
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:`,
);
});
});

View File

@@ -0,0 +1,49 @@
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].install.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));
};

View File

@@ -0,0 +1,103 @@
import { Command, Option } from "commander";
import { isRefineUptoDate } from "@commands/check-updates";
import spinner from "@utils/spinner";
import { getPreferedPM, installPackages, pmCommands } from "@utils/package";
import { promptInteractiveRefineUpdate } from "@commands/update/interactive";
import { RefinePackageInstalledVersionData } from "@definitions/package";
enum Tag {
Wanted = "wanted",
Latest = "latest",
Next = "next",
}
interface OptionValues {
tag: Tag;
dryRun: boolean;
all: boolean;
}
const load = (program: Command) => {
return program
.command("update")
.description(
"Interactively select and update all `refine` packages to selected version. To skip the interactive mode, use the `--all` option.",
)
.addOption(
new Option("-t, --tag [tag]", "Select version to update to.")
.choices(["next", "latest"])
.default(
"wanted",
"Version ranges in the `package.json` will be installed",
),
)
.option(
"-a, --all",
"Update all `refine` packages to the selected `tag`. If `tag` is not provided, version ranges in the `package.json` will be installed. This option skips the interactive mode.",
false,
)
.option(
"-d, --dry-run",
"Get outdated packages installation command without automatic updating. If `tag` is not provided, version ranges in the `package.json` will be used.",
false,
)
.action(action);
};
const action = async (options: OptionValues) => {
const { tag, dryRun, all } = options;
const packages = await spinner(isRefineUptoDate, "Checking for updates...");
if (!packages?.length) {
console.log("All `refine` packages are up to date 🎉");
return;
}
const selectedPackages = all
? runAll(tag, packages)
: await promptInteractiveRefineUpdate(packages);
if (!selectedPackages) return;
if (dryRun) {
printInstallCommand(selectedPackages);
return;
}
pmInstall(selectedPackages);
};
const runAll = (tag: Tag, packages: RefinePackageInstalledVersionData[]) => {
if (tag === Tag.Wanted) {
const isAllPackagesAtWantedVersion = packages.every(
(pkg) => pkg.current === pkg.wanted,
);
if (isAllPackagesAtWantedVersion) {
console.log(
"All `refine` packages are up to date with the wanted version 🎉",
);
return null;
}
}
const packagesWithVersion = packages.map((pkg) => {
const version = tag === Tag.Wanted ? pkg.wanted : tag;
return `${pkg.name}@${version}`;
});
return packagesWithVersion;
};
const printInstallCommand = async (packages: string[]) => {
const pm = await getPreferedPM();
const commandInstall = pmCommands[pm.name].install;
console.log(`${pm.name} ${commandInstall.join(" ")} ${packages.join(" ")}`);
};
const pmInstall = (packages: string[]) => {
console.log("Updating `refine` packages...");
console.log(packages);
installPackages(packages);
};
export default load;

View File

@@ -0,0 +1,117 @@
import { createUIGroup, validatePrompt } from ".";
test("Validate interactive prompt", () => {
const testCases = [
{
input: [
"@refinedev/airtable@1.7.8",
"@refinedev/airtable@2.7.8",
"@refinedev/airtable@3.33.0",
"@refinedev/simple-rest@2.7.8",
"@refinedev/simple-rest@3.35.2",
"@refinedev/core@3.88.4",
],
output: `You can't update the same package more than once. Please choice one.\n Duplicates: @refinedev/airtable, @refinedev/simple-rest`,
},
{
input: [],
output: true,
},
];
testCases.forEach((testCase) => {
const result = validatePrompt(testCase.input);
expect(result).toEqual(testCase.output);
});
});
test("Categorize UI Group", () => {
const testCases = [
{
input: [],
output: null,
},
{
input: [
{
name: "@refinedev/airtable",
current: "2.1.1",
wanted: "2.7.8",
latest: "3.33.0",
},
{
name: "@refinedev/core",
current: "3.88.1",
wanted: "3.88.4",
latest: "3.88.4",
},
{
name: "@refinedev/react-hook-form",
current: "3.31.0",
wanted: "3.33.2",
latest: "3.33.2",
},
{
name: "@refinedev/simple-rest",
current: "2.6.0",
wanted: "2.7.8",
latest: "3.35.2",
},
{
name: "@refinedev/strapi",
current: "3.18.0",
wanted: "3.37.0",
latest: "3.37.0",
},
],
output: {
patch: [
{
name: "@refinedev/core",
from: "3.88.1",
to: "3.88.4",
},
],
minor: [
{
name: "@refinedev/airtable",
from: "2.1.1",
to: "2.7.8",
},
{
name: "@refinedev/react-hook-form",
from: "3.31.0",
to: "3.33.2",
},
{
name: "@refinedev/simple-rest",
from: "2.6.0",
to: "2.7.8",
},
{
name: "@refinedev/strapi",
from: "3.18.0",
to: "3.37.0",
},
],
major: [
{
name: "@refinedev/airtable",
from: "2.1.1",
to: "3.33.0",
},
{
name: "@refinedev/simple-rest",
from: "2.6.0",
to: "3.35.2",
},
],
},
},
];
testCases.forEach((testCase) => {
const result = createUIGroup(testCase.input);
expect(result).toEqual(testCase.output);
});
});

View File

@@ -0,0 +1,199 @@
import inquirer from "inquirer";
import semverDiff from "semver-diff";
import chalk from "chalk";
import { findDuplicates } from "@utils/array";
import { parsePackageNameAndVersion } from "@utils/package";
import { RefinePackageInstalledVersionData } from "@definitions/package";
type UIGroup = {
patch: {
name: string;
from: string;
to: string;
}[];
minor: {
name: string;
from: string;
to: string;
}[];
major: {
name: string;
from: string;
to: string;
}[];
};
export const promptInteractiveRefineUpdate = async (
packages: RefinePackageInstalledVersionData[],
) => {
const uiGroup = createUIGroup(packages);
if (!uiGroup) {
console.log("All `refine` packages are up to date. 🎉");
return;
}
const inquirerUI = createInquirerUI(uiGroup);
const answers = await inquirer.prompt<{
packages: string[];
}>([
{
type: "checkbox",
name: "packages",
message: "Choose packages to update",
pageSize: inquirerUI.pageSize,
choices: inquirerUI.choices,
validate: validatePrompt,
},
]);
return answers.packages.length > 0 ? answers.packages : null;
};
export const validatePrompt = (input: string[]) => {
const inputParsed = input.map((pckg) => {
return parsePackageNameAndVersion(pckg);
});
const names = inputParsed.map((pckg) => pckg.name);
const duplicates = findDuplicates(names);
if (duplicates.length > 0) {
return `You can't update the same package more than once. Please choice one.\n Duplicates: ${duplicates.join(
", ",
)}`;
}
return true;
};
export const createUIGroup = (
packages: RefinePackageInstalledVersionData[],
): UIGroup | null => {
if (packages.length === 0) {
return null;
}
const packagesCategorized: UIGroup = {
patch: [],
minor: [],
major: [],
};
packages.forEach((pckg) => {
const current = pckg.current;
const diffWanted = semverDiff(current, pckg.wanted) as keyof UIGroup;
const diffLatest = semverDiff(current, pckg.latest) as keyof UIGroup;
if (diffWanted === diffLatest) {
if (diffLatest) {
packagesCategorized[diffLatest].push({
name: pckg.name,
from: current,
to: pckg.latest,
});
return;
}
}
if (diffWanted) {
packagesCategorized[diffWanted].push({
name: pckg.name,
from: current,
to: pckg.wanted,
});
}
if (diffLatest) {
packagesCategorized[diffLatest].push({
name: pckg.name,
from: current,
to: pckg.latest,
});
}
});
return packagesCategorized;
};
const createInquirerUI = (uiGroup: UIGroup) => {
let maxNameLength = 0;
let maxFromLength = 0;
[uiGroup.patch, uiGroup.minor, uiGroup.major].forEach((group) => {
group.forEach((pckg) => {
if (pckg.name.length > maxNameLength) {
maxNameLength = pckg.name.length;
}
if (pckg.from.length > maxFromLength) {
maxFromLength = pckg.from.length;
}
});
});
maxNameLength += 2;
const choices: (
| inquirer.Separator
| {
name: string;
value: string;
}
)[] = [];
const packageColumnText = "Package".padEnd(maxNameLength);
const currentColumnText = "From".padEnd(maxFromLength);
const toColumnText = "To";
const header = `\n ${packageColumnText} ${currentColumnText}${toColumnText.padStart(
maxFromLength,
)}`;
choices.push(new inquirer.Separator(header));
if (uiGroup.patch.length > 0) {
choices.push(
new inquirer.Separator(chalk.reset.bold.blue("\nPatch Updates")),
);
uiGroup.patch.forEach((pckg) => {
choices.push({
name: `${pckg.name.padEnd(maxNameLength)} ${pckg.from.padStart(
maxFromLength,
)} -> ${pckg.to}`,
value: `${pckg.name}@${pckg.to}`,
});
});
}
if (uiGroup.minor.length > 0) {
choices.push(
new inquirer.Separator(chalk.reset.bold.blue("\nMinor Updates")),
);
uiGroup.minor.forEach((pckg) => {
choices.push({
name: `${pckg.name.padEnd(maxNameLength)} ${pckg.from.padStart(
maxFromLength,
)} -> ${pckg.to}`,
value: `${pckg.name}@${pckg.to}`,
});
});
}
if (uiGroup.major.length > 0) {
choices.push(
new inquirer.Separator(chalk.reset.bold.blue("\nMajor Updates")),
);
uiGroup.major.forEach((pckg) => {
choices.push({
name: `${pckg.name.padEnd(maxNameLength)} ${pckg.from.padStart(
maxFromLength,
)} -> ${pckg.to}`,
value: `${pckg.name}@${pckg.to}`,
});
});
}
const pageSize = choices.length + 6;
return { choices, pageSize };
};

View File

@@ -0,0 +1,37 @@
import { getInstalledRefinePackages } from "@utils/package";
import { Command } from "commander";
import envinfo from "envinfo";
import ora from "ora";
const whoami = (program: Command) => {
return program
.command("whoami")
.description("View the details of the development environment")
.action(action);
};
const action = async () => {
const spinner = ora("Loading environment details...").start();
const info = await envinfo.run(
{
System: ["OS", "CPU"],
Binaries: ["Node", "Yarn", "npm"],
Browsers: ["Chrome", "Firefox", "Safari"],
},
{ showNotFound: true, markdown: true },
);
const packages = (await getInstalledRefinePackages()) || [];
const packagesMarkdown = packages
.map((pkg) => {
return ` - ${pkg.name}: ${pkg.version}`;
})
.join("\n");
spinner.stop();
console.log(info);
console.log("## Refine Packages:");
console.log(packagesMarkdown);
};
export default whoami;