mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
fork refine
This commit is contained in:
73
packages/cli/src/cli.ts
Normal file
73
packages/cli/src/cli.ts
Normal 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();
|
||||
97
packages/cli/src/commands/add/create-provider.ts
Normal file
97
packages/cli/src/commands/add/create-provider.ts
Normal 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;
|
||||
};
|
||||
165
packages/cli/src/commands/add/create-resource.ts
Normal file
165
packages/cli/src/commands/add/create-resource.ts
Normal 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");
|
||||
};
|
||||
70
packages/cli/src/commands/add/index.spec.ts
Normal file
70
packages/cli/src/commands/add/index.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
98
packages/cli/src/commands/add/index.ts
Normal file
98
packages/cli/src/commands/add/index.ts
Normal 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;
|
||||
66
packages/cli/src/commands/check-updates/index.test.tsx
Normal file
66
packages/cli/src/commands/check-updates/index.test.tsx
Normal 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);
|
||||
}
|
||||
});
|
||||
95
packages/cli/src/commands/check-updates/index.tsx
Normal file
95
packages/cli/src/commands/check-updates/index.tsx
Normal 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;
|
||||
36
packages/cli/src/commands/create-resource/index.ts
Normal file
36
packages/cli/src/commands/create-resource/index.ts
Normal 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;
|
||||
225
packages/cli/src/commands/devtools/index.ts
Normal file
225
packages/cli/src/commands/devtools/index.ts
Normal 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;
|
||||
151
packages/cli/src/commands/proxy/index.ts
Normal file
151
packages/cli/src/commands/proxy/index.ts
Normal 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;
|
||||
47
packages/cli/src/commands/runner/build/index.ts
Normal file
47
packages/cli/src/commands/runner/build/index.ts
Normal 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;
|
||||
58
packages/cli/src/commands/runner/dev/index.ts
Normal file
58
packages/cli/src/commands/runner/dev/index.ts
Normal 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;
|
||||
6
packages/cli/src/commands/runner/index.ts
Normal file
6
packages/cli/src/commands/runner/index.ts
Normal 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 };
|
||||
366
packages/cli/src/commands/runner/projectScripts.test.ts
Normal file
366
packages/cli/src/commands/runner/projectScripts.test.ts
Normal 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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
75
packages/cli/src/commands/runner/projectScripts.ts
Normal file
75
packages/cli/src/commands/runner/projectScripts.ts
Normal 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";
|
||||
},
|
||||
},
|
||||
};
|
||||
47
packages/cli/src/commands/runner/run/index.ts
Normal file
47
packages/cli/src/commands/runner/run/index.ts
Normal 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;
|
||||
30
packages/cli/src/commands/runner/runScript.ts
Normal file
30
packages/cli/src/commands/runner/runScript.ts
Normal 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;
|
||||
};
|
||||
42
packages/cli/src/commands/runner/start/index.ts
Normal file
42
packages/cli/src/commands/runner/start/index.ts
Normal 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;
|
||||
34
packages/cli/src/commands/runner/utils/index.ts
Normal file
34
packages/cli/src/commands/runner/utils/index.ts
Normal 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(", ")}`;
|
||||
};
|
||||
282
packages/cli/src/commands/swizzle/index.tsx
Normal file
282
packages/cli/src/commands/swizzle/index.tsx
Normal 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;
|
||||
@@ -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:`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
};
|
||||
103
packages/cli/src/commands/update/index.ts
Normal file
103
packages/cli/src/commands/update/index.ts
Normal 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;
|
||||
117
packages/cli/src/commands/update/interactive/index.test.tsx
Normal file
117
packages/cli/src/commands/update/interactive/index.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
199
packages/cli/src/commands/update/interactive/index.ts
Normal file
199
packages/cli/src/commands/update/interactive/index.ts
Normal 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 };
|
||||
};
|
||||
37
packages/cli/src/commands/whoami/index.ts
Normal file
37
packages/cli/src/commands/whoami/index.ts
Normal 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;
|
||||
131
packages/cli/src/components/swizzle-message/index.tsx
Normal file
131
packages/cli/src/components/swizzle-message/index.tsx
Normal 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();
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./table";
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
168
packages/cli/src/components/update-warning-table/table.ts
Normal file
168
packages/cli/src/components/update-warning-table/table.ts
Normal 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;
|
||||
};
|
||||
4
packages/cli/src/definitions/announcement.ts
Normal file
4
packages/cli/src/definitions/announcement.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type Announcement = {
|
||||
hidden?: boolean;
|
||||
content: string;
|
||||
};
|
||||
6
packages/cli/src/definitions/cardinal.d.ts
vendored
Normal file
6
packages/cli/src/definitions/cardinal.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare module "cardinal" {
|
||||
export function highlight(
|
||||
code: string,
|
||||
options?: { jsx?: boolean; theme?: string; linenos?: boolean },
|
||||
): string;
|
||||
}
|
||||
5
packages/cli/src/definitions/index.ts
Normal file
5
packages/cli/src/definitions/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./projectTypes";
|
||||
export * from "./uiFrameworks";
|
||||
export * from "./package";
|
||||
export * from "./refineConfig";
|
||||
export * from "./node";
|
||||
8
packages/cli/src/definitions/node.ts
Normal file
8
packages/cli/src/definitions/node.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type NODE_ENV =
|
||||
| "development"
|
||||
| "production"
|
||||
| "test"
|
||||
| "continuous-integration"
|
||||
| "system-integration-testing"
|
||||
| "user-acceptance-testing"
|
||||
| "custom";
|
||||
23
packages/cli/src/definitions/package.ts
Normal file
23
packages/cli/src/definitions/package.ts
Normal 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;
|
||||
};
|
||||
9
packages/cli/src/definitions/projectTypes.ts
Normal file
9
packages/cli/src/definitions/projectTypes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum ProjectTypes {
|
||||
REACT_SCRIPT = "react-scripts",
|
||||
REMIX = "remix",
|
||||
NEXTJS = "nextjs",
|
||||
VITE = "vite",
|
||||
CRACO = "craco",
|
||||
PARCEL = "parcel",
|
||||
UNKNOWN = "unknown",
|
||||
}
|
||||
46
packages/cli/src/definitions/refineConfig.ts
Normal file
46
packages/cli/src/definitions/refineConfig.ts
Normal 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;
|
||||
};
|
||||
6
packages/cli/src/definitions/uiFrameworks.ts
Normal file
6
packages/cli/src/definitions/uiFrameworks.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum UIFrameworks {
|
||||
ANTD = "antd",
|
||||
MUI = "mui",
|
||||
MANTINE = "mantine",
|
||||
CHAKRA = "chakra-ui",
|
||||
}
|
||||
5
packages/cli/src/index.ts
Normal file
5
packages/cli/src/index.ts
Normal 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";
|
||||
51
packages/cli/src/telemetry/index.ts
Normal file
51
packages/cli/src/telemetry/index.ts
Normal 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) {}
|
||||
};
|
||||
111
packages/cli/src/transformers/add-devtools-component.ts
Normal file
111
packages/cli/src/transformers/add-devtools-component.ts
Normal 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 };
|
||||
};
|
||||
121
packages/cli/src/transformers/resource.ts
Normal file
121
packages/cli/src/transformers/resource.ts
Normal 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();
|
||||
}
|
||||
114
packages/cli/src/update-notifier/index.test.ts
Normal file
114
packages/cli/src/update-notifier/index.test.ts
Normal 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);
|
||||
});
|
||||
144
packages/cli/src/update-notifier/index.tsx
Normal file
144
packages/cli/src/update-notifier/index.tsx
Normal 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";
|
||||
};
|
||||
81
packages/cli/src/utils/announcement/index.tsx
Normal file
81
packages/cli/src/utils/announcement/index.tsx
Normal 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",
|
||||
}),
|
||||
);
|
||||
};
|
||||
24
packages/cli/src/utils/array/index.test.ts
Normal file
24
packages/cli/src/utils/array/index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
5
packages/cli/src/utils/array/index.ts
Normal file
5
packages/cli/src/utils/array/index.ts
Normal 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);
|
||||
};
|
||||
61
packages/cli/src/utils/compile/index.ts
Normal file
61
packages/cli/src/utils/compile/index.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
14
packages/cli/src/utils/encode/index.ts
Normal file
14
packages/cli/src/utils/encode/index.ts
Normal 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();
|
||||
};
|
||||
55
packages/cli/src/utils/env/index.test.tsx
vendored
Normal file
55
packages/cli/src/utils/env/index.test.tsx
vendored
Normal 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
47
packages/cli/src/utils/env/index.ts
vendored
Normal 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",
|
||||
};
|
||||
9
packages/cli/src/utils/marked-terminal-renderer/index.ts
Normal file
9
packages/cli/src/utils/marked-terminal-renderer/index.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
25
packages/cli/src/utils/os/index.ts
Normal file
25
packages/cli/src/utils/os/index.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
46
packages/cli/src/utils/package/index.test.tsx
Normal file
46
packages/cli/src/utils/package/index.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
248
packages/cli/src/utils/package/index.ts
Normal file
248
packages/cli/src/utils/package/index.ts
Normal 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");
|
||||
};
|
||||
79
packages/cli/src/utils/project/index.ts
Normal file
79
packages/cli/src/utils/project/index.ts
Normal 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;
|
||||
};
|
||||
67
packages/cli/src/utils/refine/index.test.tsx
Normal file
67
packages/cli/src/utils/refine/index.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
22
packages/cli/src/utils/refine/index.ts
Normal file
22
packages/cli/src/utils/refine/index.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
39
packages/cli/src/utils/resource/index.spec.ts
Normal file
39
packages/cli/src/utils/resource/index.spec.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
62
packages/cli/src/utils/resource/index.ts
Normal file
62
packages/cli/src/utils/resource/index.ts
Normal 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";
|
||||
}
|
||||
};
|
||||
13
packages/cli/src/utils/spinner/index.ts
Normal file
13
packages/cli/src/utils/spinner/index.ts
Normal 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;
|
||||
23
packages/cli/src/utils/swizzle/appendAfterImports.test.ts
Normal file
23
packages/cli/src/utils/swizzle/appendAfterImports.test.ts
Normal 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");
|
||||
|
||||
`);
|
||||
});
|
||||
});
|
||||
18
packages/cli/src/utils/swizzle/appendAfterImports.ts
Normal file
18
packages/cli/src/utils/swizzle/appendAfterImports.ts
Normal 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)
|
||||
);
|
||||
};
|
||||
8
packages/cli/src/utils/swizzle/codes.ts
Normal file
8
packages/cli/src/utils/swizzle/codes.ts
Normal 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",
|
||||
};
|
||||
16
packages/cli/src/utils/swizzle/getFileContent.ts
Normal file
16
packages/cli/src/utils/swizzle/getFileContent.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
17
packages/cli/src/utils/swizzle/getPathPrefix.ts
Normal file
17
packages/cli/src/utils/swizzle/getPathPrefix.ts
Normal 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;
|
||||
};
|
||||
205
packages/cli/src/utils/swizzle/import.test.ts
Normal file
205
packages/cli/src/utils/swizzle/import.test.ts
Normal 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());
|
||||
});
|
||||
});
|
||||
264
packages/cli/src/utils/swizzle/import.ts
Normal file
264
packages/cli/src/utils/swizzle/import.ts
Normal 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;
|
||||
};
|
||||
22
packages/cli/src/utils/swizzle/index.ts
Normal file
22
packages/cli/src/utils/swizzle/index.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
25
packages/cli/src/utils/swizzle/parseSwizzleBlocks.test.ts
Normal file
25
packages/cli/src/utils/swizzle/parseSwizzleBlocks.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
6
packages/cli/src/utils/swizzle/parseSwizzleBlocks.ts
Normal file
6
packages/cli/src/utils/swizzle/parseSwizzleBlocks.ts
Normal 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, "");
|
||||
};
|
||||
14
packages/cli/src/utils/swizzle/prettierFormat.ts
Normal file
14
packages/cli/src/utils/swizzle/prettierFormat.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
33
packages/cli/src/utils/swizzle/provideCliHelpers.ts
Normal file
33
packages/cli/src/utils/swizzle/provideCliHelpers.ts
Normal 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);
|
||||
};
|
||||
};
|
||||
46
packages/cli/src/utils/swizzle/renderCodeMarkdown.ts
Normal file
46
packages/cli/src/utils/swizzle/renderCodeMarkdown.ts
Normal 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;
|
||||
};
|
||||
29
packages/cli/src/utils/text/index.test.tsx
Normal file
29
packages/cli/src/utils/text/index.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
10
packages/cli/src/utils/text/index.ts
Normal file
10
packages/cli/src/utils/text/index.ts
Normal 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,
|
||||
"",
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user