mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
fork refine
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user