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

73
packages/cli/src/cli.ts Normal file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env node
import { Command } from "commander";
import figlet from "figlet";
import checkUpdates from "@commands/check-updates";
import createResource from "@commands/create-resource";
import proxy from "@commands/proxy";
import { build, dev, run, start } from "@commands/runner";
import swizzle from "@commands/swizzle";
import update from "@commands/update";
import whoami from "@commands/whoami";
import devtools from "@commands/devtools";
import add from "@commands/add";
import { telemetryHook } from "@telemetryindex";
import { printAnnouncements } from "@utils/announcement";
import "@utils/env";
// It reads and updates from package.json during build. ref: tsup.config.ts
const REFINE_CLI_VERSION = "1.0.0";
const bootstrap = () => {
const program = new Command();
program
.version(
`@refinedev/cli@${REFINE_CLI_VERSION}`,
"-v, --version",
"Output the current version.",
)
.description(
figlet.textSync("refine", {
font: "Isometric1",
horizontalLayout: "full",
verticalLayout: "full",
whitespaceBreak: true,
}),
)
.usage("<command> [options]")
.helpOption("-h, --help", "Output usage information.");
// load commands
swizzle(program);
createResource(program);
update(program);
dev(program);
build(program);
start(program);
run(program);
checkUpdates(program);
whoami(program);
proxy(program);
devtools(program);
add(program);
program.hook("preAction", printAnnouncements);
program.hook("postAction", (thisCommand) => {
const command = thisCommand.args[0];
if (["run"].includes(command)) return;
telemetryHook();
});
program.parse(process.argv);
if (!process.argv.slice(2).length) {
program.outputHelp();
}
};
bootstrap();

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;

View File

@@ -0,0 +1,131 @@
import React from "react";
import dedent from "dedent";
import { SWIZZLE_CODES } from "@utils/swizzle/codes";
import chalk from "chalk";
import { markedTerminalRenderer } from "@utils/marked-terminal-renderer";
type Params = {
label: string;
files: [targetPath: string, statusCode: string][];
message?: string;
};
export const printSwizzleMessage = ({
label,
files,
message = "**`Warning:`** You should use the component where you want to use it.",
}: Params) => {
const errors = files.filter(([, statusCode]) =>
Object.values(SWIZZLE_CODES)
.filter((code) => code !== SWIZZLE_CODES.SUCCESS)
.includes(statusCode),
);
let status = "success";
switch (errors.length) {
// no errors
case 0:
status = "success";
break;
// all errors
case files.length:
status = "error";
break;
// some errors
default:
status = "warning";
break;
}
const clearFilePath = (filePath: string) => {
const relative = filePath?.replace(process.cwd(), "");
if (relative?.startsWith("/")) {
return relative.slice(1);
}
if (relative?.startsWith("./")) {
return relative.slice(2);
}
return relative;
};
const printStatusMessage = () => {
switch (status) {
case "success":
console.log(
chalk.blueBright(
`Successfully swizzled ${chalk.bold(label)}`,
),
);
return;
case "warning":
console.log(
chalk.yellowBright(
`Swizzle completed with errors for ${chalk.bold(
label,
)}`,
),
);
return;
case "error":
console.log(
chalk.redBright(`Swizzle failed for ${chalk.bold(label)}`),
);
return;
default:
return;
}
};
const printFiles = () => {
const chalks = [];
chalks.push(chalk.dim(`File${files.length > 1 ? "s" : ""} created:`));
chalks.push(
files
.map(([targetPath, statusCode]) => {
if (statusCode === SWIZZLE_CODES.SUCCESS) {
return chalk.cyanBright.dim(
` - ${clearFilePath(targetPath)}`,
);
}
if (statusCode === SWIZZLE_CODES.TARGET_ALREADY_EXISTS) {
return chalk.cyanBright.dim(
` - ${chalk.yellowBright.bold(
"[FILE_ALREADY_EXISTS] ",
)}${clearFilePath(targetPath)}`,
);
}
if (statusCode === SWIZZLE_CODES.SOURCE_PATH_NOT_A_FILE) {
return chalk.cyanBright.dim(
` - ${chalk.yellowBright.bold(
"[SOURCE NOT FOUND] ",
)}${clearFilePath(targetPath)}`,
);
}
return chalk.cyanBright.dim(
` - ${chalk.yellowBright.bold(
`[${statusCode}]`,
)}${clearFilePath(targetPath)}`,
);
})
.join("\n"),
);
console.log(chalks.join("\n"));
};
const printSwizzleMessage = () => {
if (message && status !== "error") {
console.log(markedTerminalRenderer(dedent("\n" + message)));
}
};
console.log("");
printStatusMessage();
printFiles();
console.log("");
printSwizzleMessage();
};

View File

@@ -0,0 +1 @@
export * from "./table";

View File

@@ -0,0 +1,52 @@
import * as packageUtils from "@utils/package";
import { getInstallCommand } from "./table";
test("Update warning npm command", async () => {
const testCases: {
output: string;
dependencies: string[];
scripts: Record<string, string>;
}[] = [
// have script, have dependency
{
output: "npm run refine update",
dependencies: ["@refinedev/cli"],
scripts: {
refine: "refine",
},
},
// has script, no dependency
{
output: "npm run refine update",
dependencies: ["@pankod/tefine-cli"],
scripts: {
refine: "refine",
},
},
// no script, has dependency
{
output: "npx refine update",
dependencies: ["@refinedev/cli"],
scripts: {
tefine: "refine",
},
},
// no script, no dependency
{
output: "npx @refinedev/cli update",
dependencies: [],
scripts: {},
},
];
for (const testCase of testCases) {
jest.spyOn(packageUtils, "getDependencies").mockReturnValue(
testCase.dependencies,
);
jest.spyOn(packageUtils, "getScripts").mockReturnValue(
testCase.scripts,
);
expect(await getInstallCommand()).toBe(testCase.output);
}
});

View File

@@ -0,0 +1,168 @@
import React from "react";
import { RefinePackageInstalledVersionData } from "@definitions/package";
import Table from "cli-table3";
import chalk from "chalk";
import center from "center-align";
import { getDependencies, getPreferedPM, getScripts } from "@utils/package";
import { removeANSIColors } from "@utils/text";
const columns = {
name: "name",
current: "current",
wanted: "wanted",
latest: "latest",
changelog: "changelog",
} as const;
const orderedColumns: (typeof columns)[keyof typeof columns][] = [
columns.name,
columns.current,
columns.wanted,
columns.latest,
columns.changelog,
];
export interface UpdateWarningTableParams {
data: RefinePackageInstalledVersionData[];
}
export const printUpdateWarningTable = async (
params: UpdateWarningTableParams,
) => {
const data = params?.data;
const tableHead = Object.keys(data?.[0] || {});
if (!data || !tableHead.length) return;
const table = new Table({
head: orderedColumns,
style: {
head: ["blue"],
},
});
data.forEach((row) => {
table.push(
orderedColumns.map((column) => {
const columnValue = row[column];
if (!columnValue) return columnValue;
if (column === "latest" || column === "wanted") {
const installedVersion = parseVersions(row.current);
const latestVersion = parseVersions(columnValue);
const colors = getColorsByVersionDiffrence(
installedVersion,
latestVersion,
);
const textMajor = chalk[colors.major](latestVersion.major);
const textMinor = chalk[colors.minor](latestVersion.minor);
const textPatch = chalk[colors.patch](latestVersion.patch);
return `${textMajor}.${textMinor}.${textPatch}`;
}
if (column === "changelog") {
return chalk.blueBright.underline(columnValue);
}
return columnValue;
}),
);
});
const tableOutput = table.toString();
const tableWidth = removeANSIColors(
tableOutput.split("\n")?.[0] || "",
).length;
console.log();
console.log(center("Update Available", tableWidth));
console.log(tableOutput);
console.log(
center(
`To update ${chalk.bold("`refine`")} packages with wanted version`,
tableWidth,
),
);
console.log(
center(
` Run the following command: ${chalk.yellowBright(
await getInstallCommand(),
)}`,
tableWidth,
),
);
console.log();
};
const parseVersions = (text: string) => {
const versions = text.split(".");
return {
major: versions[0],
minor: versions[1],
patch: versions[2],
};
};
const getColorsByVersionDiffrence = (
installedVersion: ReturnType<typeof parseVersions>,
nextVersion: ReturnType<typeof parseVersions>,
) => {
const isMajorDiffrence =
installedVersion.major.trim() !== nextVersion.major.trim();
if (isMajorDiffrence)
return {
major: "red",
minor: "red",
patch: "red",
} as const;
const isMinorDiffrence =
installedVersion.minor.trim() !== nextVersion.minor.trim();
if (isMinorDiffrence)
return {
major: "white",
minor: "yellow",
patch: "yellow",
} as const;
const isPatchDiffrence =
installedVersion.patch.trim() !== nextVersion.patch.trim();
if (isPatchDiffrence)
return {
major: "white",
minor: "white",
patch: "green",
} as const;
return {
major: "white",
minor: "white",
patch: "white",
} as const;
};
export const getInstallCommand = async () => {
const fallbackCommand = `npx @refinedev/cli update`;
const dependencies = getDependencies();
const scriptKeys = Object.keys(getScripts());
const hasCli = dependencies.includes("@refinedev/cli");
const hasScript = scriptKeys.includes("refine");
if (!hasCli && !hasScript) {
return fallbackCommand;
}
const pm = await getPreferedPM();
if (hasScript) {
return `${pm.name} run refine update`;
}
if (hasCli) {
return `npx refine update`;
}
return fallbackCommand;
};

View File

@@ -0,0 +1,4 @@
export type Announcement = {
hidden?: boolean;
content: string;
};

View File

@@ -0,0 +1,6 @@
declare module "cardinal" {
export function highlight(
code: string,
options?: { jsx?: boolean; theme?: string; linenos?: boolean },
): string;
}

View File

@@ -0,0 +1,5 @@
export * from "./projectTypes";
export * from "./uiFrameworks";
export * from "./package";
export * from "./refineConfig";
export * from "./node";

View File

@@ -0,0 +1,8 @@
export type NODE_ENV =
| "development"
| "production"
| "test"
| "continuous-integration"
| "system-integration-testing"
| "user-acceptance-testing"
| "custom";

View File

@@ -0,0 +1,23 @@
export enum PackageManagerTypes {
NPM = "npm",
YARN = "yarn",
PNPM = "pnpm",
}
export type NpmOutdatedResponse = Record<
string,
{
current: string;
wanted: string;
latest: string;
dependet?: string;
}
>;
export type RefinePackageInstalledVersionData = {
name: string;
current: string;
wanted: string;
latest: string;
changelog?: string;
};

View File

@@ -0,0 +1,9 @@
export enum ProjectTypes {
REACT_SCRIPT = "react-scripts",
REMIX = "remix",
NEXTJS = "nextjs",
VITE = "vite",
CRACO = "craco",
PARCEL = "parcel",
UNKNOWN = "unknown",
}

View File

@@ -0,0 +1,46 @@
export type SwizzleFile = {
/**
* Group name of the item to group by
*/
group: string;
/**
* Name of the item to display
*/
label: string;
/**
* Array of files with source and destination. `transform` can also be provided to perform transform actions specific to the file.
*/
files: {
src: string;
dest: string;
transform?: (content: string) => string;
}[];
/**
* Success message shown after swizzle is complete. Supports markdown features.
*/
message?: string;
/**
* Array of packages to install after swizzling
*/
requiredPackages?: string[];
};
export type SwizzleConfig = {
/**
* Array of swizzle items
*/
items: Array<SwizzleFile>;
/**
* Transform function to perform on every swizzled file
*/
transform?: (content: string, src: string, dest: string) => string;
};
export type RefineConfig = {
name?: string;
group?: string;
/**
* Swizzle configuration of the package
*/
swizzle: SwizzleConfig;
};

View File

@@ -0,0 +1,6 @@
export enum UIFrameworks {
ANTD = "antd",
MUI = "mui",
MANTINE = "mantine",
CHAKRA = "chakra-ui",
}

View File

@@ -0,0 +1,5 @@
export * from "./definitions";
export { getImports, getNameChangeInImport } from "./utils/swizzle/import";
export { appendAfterImports } from "./utils/swizzle/appendAfterImports";
export { getFileContent } from "./utils/swizzle/getFileContent";
export type { ImportMatch, NameChangeMatch } from "./utils/swizzle/import";

View File

@@ -0,0 +1,51 @@
import { NODE_ENV } from "@definitions/node";
import { ProjectTypes } from "@definitions/projectTypes";
import { ENV } from "@utils/env";
import { getOS } from "@utils/os";
import { getInstalledRefinePackages, getRefineProjectId } from "@utils/package";
import { getProjectType } from "@utils/project";
import fetch from "node-fetch";
interface TelemetryData {
nodeEnv?: NODE_ENV;
nodeVersion: string;
os: string;
osVersion: string;
command: string;
packages: {
name: string;
version: string;
}[];
projectFramework: ProjectTypes;
}
export const getTelemetryData = async (): Promise<TelemetryData> => {
const os = await getOS();
const data = {
nodeEnv: ENV.NODE_ENV,
nodeVersion: process.version,
os: os.name,
osVersion: os.version,
command: process.argv[2],
packages: (await getInstalledRefinePackages()) || [],
projectFramework: getProjectType(),
projectId: getRefineProjectId(),
};
return data;
};
export const telemetryHook = async () => {
if (ENV.REFINE_NO_TELEMETRY === "true") return;
try {
const data = await getTelemetryData();
await fetch("https://telemetry.refine.dev/cli", {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
} catch (error) {}
};

View File

@@ -0,0 +1,111 @@
import {
API,
FileInfo,
JSCodeshift,
JSXElement,
ASTPath,
Collection,
} from "jscodeshift";
import execa from "execa";
import { prettierFormat } from "../utils/swizzle/prettierFormat";
export const parser = "tsx";
// runs .bin/jscodeshift with the default export transformer on the current directory
export const addDevtoolsComponent = async () => {
const jscodeshiftExecutable = require.resolve(".bin/jscodeshift");
const { stderr } = execa.sync(jscodeshiftExecutable, [
"./",
"--extensions=ts,tsx,js,jsx",
"--parser=tsx",
`--transform=${__dirname}/../src/transformers/add-devtools-component.ts`,
`--ignore-pattern=**/.cache/**`,
`--ignore-pattern=**/node_modules/**`,
`--ignore-pattern=**/build/**`,
`--ignore-pattern=**/.next/**`,
]);
if (stderr) {
console.log(stderr);
}
};
export default async function transformer(file: FileInfo, api: API) {
const j = api.jscodeshift;
const source = j(file.source);
const refineElement = source.find(j.JSXElement, {
openingElement: {
name: {
name: "Refine",
},
},
});
const hasRefineElement = refineElement.length !== 0;
if (!hasRefineElement) {
return;
}
if (hasDevtoolsImport(j, source) && hasDevtoolsProvider(j, source)) {
return;
}
addDevtoolsImport(j, source);
refineElement.forEach((path) => {
wrapWithDevtoolsProvider(j, path);
});
return await prettierFormat(source.toSource());
}
export const hasDevtoolsImport = (j: JSCodeshift, source: Collection) => {
return source.find(j.ImportDeclaration, {
source: {
value: "@refinedev/devtools",
},
}).length;
};
export const hasDevtoolsProvider = (j: JSCodeshift, source: Collection) => {
return source.find(j.JSXElement, {
openingElement: {
name: {
name: "DevtoolsProvider",
},
},
}).length;
};
export const addDevtoolsImport = (j: JSCodeshift, source: Collection) => {
const devtoolsImport = j.importDeclaration(
[
j.importSpecifier(j.identifier("DevtoolsProvider")),
j.importSpecifier(j.identifier("DevtoolsPanel")),
],
j.literal("@refinedev/devtools"),
);
source.get().node.program.body.unshift(devtoolsImport);
};
const wrapWithDevtoolsProvider = (
j: JSCodeshift,
refineEelement: ASTPath<JSXElement>,
) => {
const panel = j.jsxElement(
j.jsxOpeningElement(j.jsxIdentifier("DevtoolsPanel")),
);
panel.openingElement.selfClosing = true;
const provider = j.jsxElement(
j.jsxOpeningElement(j.jsxIdentifier("DevtoolsProvider")),
j.jsxClosingElement(j.jsxIdentifier("DevtoolsProvider")),
// Pass in the refineEelement component as children
[refineEelement.value, panel],
);
j(refineEelement).replaceWith(provider);
return { panel, provider };
};

View File

@@ -0,0 +1,121 @@
import {
API,
FileInfo,
JSXAttribute,
JSXExpressionContainer,
ArrayExpression,
} from "jscodeshift";
export const parser = "tsx";
export default function transformer(file: FileInfo, api: API, options: any) {
const j = api.jscodeshift;
const source = j(file.source);
const rootElement = source.find(j.JSXElement, {
openingElement: {
name: {
name: "Refine",
},
},
});
if (rootElement.length === 0) {
return;
}
// prepare actions
const actions = options.__actions.split(",");
const getPath = (resourceName: string, action: string) => {
if (action === "list") {
return `/${resourceName}`;
}
if (action === "create") {
return `/${resourceName}/create`;
}
if (action === "edit") {
return `/${resourceName}/edit/:id`;
}
if (action === "show") {
return `/${resourceName}/show/:id`;
}
return `/${resourceName}`;
};
const resourceProperty = [
j.property(
"init",
j.identifier("name"),
j.stringLiteral(options.__resourceName),
),
];
actions.map((item: string) => {
resourceProperty.push(
j.property(
"init",
j.identifier(item),
j.stringLiteral(getPath(options.__resourceName, item)),
),
);
});
rootElement.replaceWith((path) => {
const attributes = path.node.openingElement.attributes;
if (!attributes) {
return path.node;
}
const resourcePropIndex = attributes.findIndex(
(attr) =>
attr.type === "JSXAttribute" && attr.name.name === "resources",
);
const resourceObjectExpression = j.objectExpression(resourceProperty);
// if no resources prop, add it
if (resourcePropIndex === -1) {
attributes.push(
j.jsxAttribute(
j.jsxIdentifier("resources"),
j.jsxExpressionContainer(
j.arrayExpression([resourceObjectExpression]),
),
),
);
return path.node;
}
const resourceValue = (attributes[resourcePropIndex] as JSXAttribute)
.value as JSXExpressionContainer;
// resources={RESOURCE_CONSTANT} => resources={[...RESOURCE_CONSTANT, {name: "post", list: List}]}
if (resourceValue.expression.type === "Identifier") {
attributes[resourcePropIndex] = j.jsxAttribute(
j.jsxIdentifier("resources"),
j.jsxExpressionContainer(
j.arrayExpression([
j.spreadElement(resourceValue.expression),
resourceObjectExpression,
]),
),
);
return path.node;
}
// resources={[...resources]} => resources={[...resources, {name: "post", list: List}]}
const resourceArray = resourceValue.expression as ArrayExpression;
resourceArray.elements.push(resourceObjectExpression);
return path.node;
});
return source.toSource();
}

View File

@@ -0,0 +1,114 @@
import { ENV } from "@utils/env";
import * as notifier from "./index";
const {
store,
isPackagesCacheExpired,
isUpdateNotifierDisabled,
shouldUpdatePackagesCache,
} = notifier;
test("Should update packages cache", async () => {
const testCases = [
{
isExpired: true,
isKeyValid: true,
output: true,
},
{
isExpired: true,
isKeyValid: false,
output: true,
},
{
isExpired: false,
isKeyValid: false,
output: true,
},
{
isExpired: false,
isKeyValid: true,
output: false,
},
{
isExpired: false,
isKeyValid: null,
output: null,
},
{
isExpired: true,
isKeyValid: null,
output: null,
},
];
for (const testCase of testCases) {
jest.spyOn(notifier, "isPackagesCacheExpired").mockReturnValueOnce(
testCase.isExpired,
);
jest.spyOn(notifier, "validateKey").mockResolvedValue(
testCase.isKeyValid,
);
const shouldUpdate = await shouldUpdatePackagesCache();
expect(shouldUpdate).toBe(testCase.output);
}
});
test("Package cache is expired", () => {
const testCases = [
{
lastUpdated: 1,
now: 2,
cacheTTL: "1",
output: true,
},
{
lastUpdated: 1,
now: 2,
cacheTTL: "2",
output: false,
},
{
lastUpdated: 1,
now: 2,
cacheTTL: "1",
output: true,
},
];
testCases.forEach((testCase) => {
ENV.UPDATE_NOTIFIER_CACHE_TTL = testCase.cacheTTL;
Date.now = jest.fn(() => testCase.now);
store.get = jest.fn(() => testCase.lastUpdated);
expect(isPackagesCacheExpired()).toBe(testCase.output);
});
store.get = jest.fn(() => undefined);
expect(isPackagesCacheExpired()).toBe(true);
store.get = jest.fn(() => null);
expect(isPackagesCacheExpired()).toBe(true);
store.get = jest.fn(() => 0);
expect(isPackagesCacheExpired()).toBe(true);
});
test("Update notifier should not run if env.UPDATE_NOTIFIER_IS_DISABLED is true", () => {
ENV.UPDATE_NOTIFIER_IS_DISABLED = "true";
expect(isUpdateNotifierDisabled()).toBe(true);
ENV.UPDATE_NOTIFIER_IS_DISABLED = "TRUE";
expect(isUpdateNotifierDisabled()).toBe(true);
ENV.UPDATE_NOTIFIER_IS_DISABLED = "false";
expect(isUpdateNotifierDisabled()).toBe(false);
ENV.UPDATE_NOTIFIER_IS_DISABLED = "1";
expect(isUpdateNotifierDisabled()).toBe(false);
ENV.UPDATE_NOTIFIER_IS_DISABLED = "0";
expect(isUpdateNotifierDisabled()).toBe(false);
});

View File

@@ -0,0 +1,144 @@
import Conf from "conf";
import chalk from "chalk";
import { isRefineUptoDate } from "@commands/check-updates";
import { printUpdateWarningTable } from "@components/update-warning-table";
import { RefinePackageInstalledVersionData } from "@definitions/package";
import { getInstalledRefinePackages } from "@utils/package";
import { ENV } from "@utils/env";
import { stringToBase64 } from "@utils/encode";
const STORE_NAME = "refine-update-notifier";
export interface Store {
key: string;
lastUpdated: number;
packages: RefinePackageInstalledVersionData[];
}
export const store = new Conf<Store>({
projectName: STORE_NAME,
defaults: {
key: "",
lastUpdated: 0,
packages: [],
},
});
// update notifier should not throw any unhandled error to prevent breaking user workflow.
export const updateNotifier = async () => {
if (isUpdateNotifierDisabled()) return;
const shouldUpdate = await shouldUpdatePackagesCache();
if (shouldUpdate === null) return;
if (shouldUpdate) {
updatePackagesCache();
return;
}
showWarning();
updatePackagesCache();
};
/**
* renders outdated packages table if there is any
*/
const showWarning = async () => {
const packages = store.get("packages");
if (!packages?.length) return;
await printUpdateWarningTable({ data: packages });
console.log("\n");
};
/**
* @returns `null` It's mean something went wrong while checking key or cache. so we should not update cache.
* @returns `boolean` if cache should be updated or not
* if cache is expired or key is invalid, update cache in background and not show warning
*/
export const shouldUpdatePackagesCache = async () => {
const isKeyValid = await validateKey();
const isExpired = isPackagesCacheExpired();
if (isKeyValid === null) return null;
if (isExpired || !isKeyValid) return true;
return false;
};
/**
* @returns `null` something went wrong
* @returns `packages` if packages updated
*/
export const updatePackagesCache = async () => {
try {
const packages = await isRefineUptoDate();
store.set("packages", packages);
store.set("lastUpdated", Date.now());
store.set("key", await generateKeyFromPackages());
return packages;
} catch (error) {
// invalidate store
store.set("packages", []);
store.set("lastUpdated", Date.now());
store.set("key", "");
return null;
}
};
export const isPackagesCacheExpired = () => {
const lastUpdated = store.get("lastUpdated");
if (!lastUpdated) return true;
const now = Date.now();
const diff = now - lastUpdated;
const cacheTTL = Number(ENV.UPDATE_NOTIFIER_CACHE_TTL);
return diff >= cacheTTL;
};
/**
* @returns `true` if key is valid
* @returns `false` if key is invalid
* @returns `null` if there is an error
*/
export const validateKey = async () => {
const key = store.get("key");
const newKey = await generateKeyFromPackages();
if (newKey === null) return null;
return key === newKey;
};
/**
* @returns `null` if there is an error
* @returns `string` if key is generated
*/
export const generateKeyFromPackages = async () => {
const packages = await getInstalledRefinePackages();
if (!packages) {
console.error(
chalk.red(
`Something went wrong when trying to get installed \`refine\` packages.`,
),
);
return null;
}
const currentVersionsWithName = packages.map(
(p) => `${p.name}@${p.version}`,
);
const hash = stringToBase64(currentVersionsWithName.toString());
return hash;
};
export const isUpdateNotifierDisabled = () => {
return ENV.UPDATE_NOTIFIER_IS_DISABLED.toLocaleLowerCase() === "true";
};

View File

@@ -0,0 +1,81 @@
import React from "react";
import matter from "gray-matter";
import boxen from "boxen";
import { 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,61 @@
import Handlebars from "handlebars";
import {
readFileSync,
readdirSync,
createFileSync,
writeFileSync,
unlinkSync,
} from "fs-extra";
export const compile = (filePath: string, params: any): string => {
const content = readFileSync(filePath);
Handlebars.registerHelper("ifIn", function (elem, list, options) {
if (elem.includes(list)) {
return options.fn(Handlebars);
}
return options.inverse(Handlebars);
});
Handlebars.registerHelper("formatInferencerComponent", function (string) {
if (!string) {
return;
}
switch (string) {
case "chakra-ui":
return "ChakraUI";
default:
return string.charAt(0).toUpperCase() + string.slice(1);
}
});
Handlebars.registerHelper("capitalize", function (string) {
if (!string) {
return;
}
return string.charAt(0).toUpperCase() + string.slice(1);
});
const template = Handlebars.compile(content.toString());
return template(params);
};
export const compileDir = (dirPath: string, params: any) => {
const files = readdirSync(dirPath);
files.forEach((file: string) => {
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);
}
});

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

@@ -0,0 +1,47 @@
import { NODE_ENV } from "@definitions/node";
import * as dotenv from "dotenv";
dotenv.config();
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;
};
export const ENV = {
NODE_ENV: getNodeEnv(),
REFINE_NO_TELEMETRY: process.env.REFINE_NO_TELEMETRY || "false",
UPDATE_NOTIFIER_IS_DISABLED:
process.env.UPDATE_NOTIFIER_IS_DISABLED || "false",
UPDATE_NOTIFIER_CACHE_TTL:
process.env.UPDATE_NOTIFIER_CACHE_TTL || 1000 * 60 * 60 * 24, // 24 hours,
REFINE_PROXY_DOMAIN:
process.env.REFINE_PROXY_DOMAIN || "https://refine.dev",
REFINE_PROXY_TARGET:
process.env.REFINE_PROXY_TARGET || "http://localhost:3000",
REFINE_PROXY_PORT: process.env.REFINE_PROXY_PORT || "7313",
REFINE_PROXY_REWRITE_URL:
process.env.REFINE_REWRITE_URL || "http://localhost:7313",
};

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,248 @@
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";
// TODO: Add package.json type
export const getPackageJson = (): any => {
if (!existsSync("package.json")) {
throw new Error("./package.json not found");
}
return JSON.parse(readFileSync("package.json", "utf8"));
};
export const getDependencies = (): string[] => {
const packageJson = getPackageJson();
return Object.keys(packageJson.dependencies || {});
};
export const getDependenciesWithVersion = (): string[] => {
const packageJson = getPackageJson();
return packageJson.dependencies;
};
export const getDevDependencies = (): string[] => {
const packageJson = getPackageJson();
return Object.keys(packageJson.devDependencies || {});
};
export const getScripts = (): Record<string, string> => {
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: {
install: ["install", "--save"],
installDev: ["install", "--save-dev"],
outdatedJson: ["outdated", "--json"],
},
yarn: {
install: ["add"],
installDev: ["add", "-D"],
outdatedJson: ["outdated", "--json"],
},
pnpm: {
install: ["add"],
installDev: ["add", "-D"],
outdatedJson: ["outdated", "--format", "json"],
},
};
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[]) => {
const pm = await getPreferedPM();
try {
const installCommand = pmCommands[pm.name].install;
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("All `refine` packages updated 🎉");
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].install;
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");
};

View File

@@ -0,0 +1,79 @@
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
const dependencies = getDependencies();
const devDependencies = getDevDependencies();
// 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")
) {
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;
};

View File

@@ -0,0 +1,67 @@
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(
testCase.input,
);
const result = hasDefaultScript();
expect(result).toEqual(testCase.output);
});
});

View File

@@ -0,0 +1,22 @@
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,39 @@
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.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,62 @@
import { ProjectTypes } from "@definitions/projectTypes";
export const getResourcePath = (
projectType: ProjectTypes,
): { path: string; alias: string } => {
switch (projectType) {
case ProjectTypes.NEXTJS:
return {
path: "src/components",
alias: "../src/components",
};
case ProjectTypes.REMIX:
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:
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:
return "./app";
case ProjectTypes.NEXTJS:
case ProjectTypes.REACT_SCRIPT:
case ProjectTypes.VITE:
case ProjectTypes.CRACO:
case ProjectTypes.PARCEL:
case ProjectTypes.UNKNOWN:
default:
return "./src";
}
};

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,18 @@
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 { 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,205 @@
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";
`;
const expected = [
{
statement: 'import React from "react";',
importPath: "react",
defaultImport: "React",
},
{
statement: 'import { Button } from "antd";',
importPath: "antd",
namedImports: " { Button }",
},
{
statement: 'import { TextInput as AntTextInput } from "antd";',
importPath: "antd",
namedImports: " { TextInput as AntTextInput }",
},
{
statement: 'import * as Antd from "antd";',
importPath: "antd",
namespaceImport: "Antd",
},
{
statement:
'import { Button as AntButton, TextInput } from "antd";',
importPath: "antd",
namedImports: " { Button as AntButton, TextInput }",
},
{
statement:
'import { Button as AntButton, TextInput as AntTextInput } from "antd";',
importPath: "antd",
namedImports:
" { Button as AntButton, TextInput as AntTextInput }",
},
];
expect(getImports(content)).toEqual(expected);
});
});
describe("getNameChangeInImport", () => {
it("should get all name changes", () => {
const statement = `
{ Button as AntButton, TextInput as AntTextInput }
`;
const expected = [
{
statement: " Button as AntButton,",
fromName: "Button",
toName: "AntButton",
afterCharacter: ",",
},
{
statement: " TextInput as AntTextInput ",
fromName: "TextInput",
toName: "AntTextInput",
afterCharacter: undefined,
},
];
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());
});
});

View File

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

View File

@@ -0,0 +1,22 @@
import path from "path";
import { RefineConfig } from "@definitions";
import { provideCliHelpers } from "./provideCliHelpers";
export const getRefineConfig = async (
packagePath: string,
isAbsolute?: boolean,
) => {
try {
provideCliHelpers(packagePath, isAbsolute);
// eslint-disable-next-line @typescript-eslint/no-var-requires
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,33 @@
import path from "path";
import * as RefineCLI from "../../index";
import { getFileContent } from "./getFileContent";
// eslint-disable-next-line @typescript-eslint/no-var-requires
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,10 @@
export const uppercaseFirstChar = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
export const removeANSIColors = (str: string): string => {
return str.replace(
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
"",
);
};