mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
packages
This commit is contained in:
11
packages/cli/.npmignore
Normal file
11
packages/cli/.npmignore
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
test
|
||||
jest.config.js
|
||||
**/*.spec.ts
|
||||
**/*.spec.tsx
|
||||
**/*.test.ts
|
||||
**/*.test.tsx
|
||||
tsup.config.ts
|
||||
tsconfig.test.json
|
||||
tsconfig.declarations.json
|
||||
1
packages/cli/.npmrc
Normal file
1
packages/cli/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
legacy-peer-deps=true
|
||||
17
packages/cli/ANNOUNCEMENTS.md
Normal file
17
packages/cli/ANNOUNCEMENTS.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---announcement
|
||||
hidden: false
|
||||
|
||||
---
|
||||
|
||||
Refine Devtools beta version is out! To install in your project, just run `npm run refine devtools init`. https://s.refine.dev/devtools-beta
|
||||
|
||||
---
|
||||
|
||||
---announcement
|
||||
hidden: false
|
||||
|
||||
---
|
||||
|
||||
Hello from Refine team! Hope you enjoy! Join our Discord community to get help and discuss with other users. https://discord.gg/refine
|
||||
|
||||
---
|
||||
1078
packages/cli/CHANGELOG.md
Normal file
1078
packages/cli/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
31
packages/cli/README.md
Normal file
31
packages/cli/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
<div align="center" style="margin: 30px;">
|
||||
<a href="https://refine.dev">
|
||||
<img alt="refine logo" src="https://refine.ams3.cdn.digitaloceanspaces.com/readme/refine-readme-banner.png">
|
||||
</a>
|
||||
</div>
|
||||
<br/>
|
||||
<div align="center">refine is an open-source, headless React framework for developers building enterprise web applications.
|
||||
|
||||
It eliminates repetitive tasks in CRUD operations and provides industry-standard solutions for critical project components like **authentication**, **access control**, **routing**, **networking**, **state management**, and **i18n**.
|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
<sub>Created by <a href="https://refine.dev">refine</a></sub>
|
||||
</div>
|
||||
|
||||
## About
|
||||
|
||||
[refine](https://refine.dev/) is **headless by design**, offering unlimited styling and customization options. Moreover, refine ships with ready-made integrations for [Ant Design](https://ant.design/), [Material UI](https://mui.com/material-ui/getting-started/overview/), [Mantine](https://mantine.dev/), and [Chakra UI](https://chakra-ui.com/) for convenience.
|
||||
|
||||
refine has connectors for 15+ backend services, including REST API, [GraphQL](https://graphql.org/), and popular services like [Airtable](https://www.airtable.com/), [Strapi](https://strapi.io/), [Supabase](https://supabase.com/), [Firebase](https://firebase.google.com/), and [Directus](https://directus.io/)
|
||||
|
||||
[Refer to documentation for more info about refine→](https://refine.dev/docs/)
|
||||
[Step up to refine tutorials →](https://refine.dev/docs/tutorial/introduction/index/)
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install @refinedev/cli
|
||||
```
|
||||
16
packages/cli/jest.config.js
Normal file
16
packages/cli/jest.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
const { compilerOptions } = require("./tsconfig");
|
||||
|
||||
/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
rootDir: "./",
|
||||
displayName: "refine-cli",
|
||||
setupFilesAfterEnv: ["<rootDir>/test/jest.setup.ts"],
|
||||
testEnvironment: "node",
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths),
|
||||
modulePaths: ["<rootDir>", "src"],
|
||||
moduleDirectories: ["node_modules", "src"],
|
||||
resetMocks: true,
|
||||
clearMocks: true,
|
||||
};
|
||||
101
packages/cli/package.json
Normal file
101
packages/cli/package.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "@refinedev/cli",
|
||||
"version": "2.16.39",
|
||||
"private": false,
|
||||
"description": "refine is a React-based framework for building internal tools, rapidly. It ships with Ant Design System, an enterprise-level UI toolkit.",
|
||||
"license": "MIT",
|
||||
"author": "refine",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.mjs",
|
||||
"typings": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"refine": "./dist/cli.cjs"
|
||||
},
|
||||
"scripts": {
|
||||
"attw": "attw --pack .",
|
||||
"build": "tsup && node ../shared/generate-declarations.js",
|
||||
"dev": "tsup --watch",
|
||||
"prepare": "pnpm build",
|
||||
"publint": "publint --strict=true --level=suggestion",
|
||||
"test": "jest --passWithNoTests --runInBand",
|
||||
"types": "node ../shared/generate-declarations.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@npmcli/package-json": "^5.2.0",
|
||||
"@refinedev/devtools-server": "1.1.37",
|
||||
"boxen": "^5.1.2",
|
||||
"camelcase": "^6.2.0",
|
||||
"cardinal": "^2.1.1",
|
||||
"center-align": "1.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"cli-table3": "^0.6.3",
|
||||
"commander": "9.4.1",
|
||||
"conf": "^10.2.0",
|
||||
"decamelize": "^5.0.0",
|
||||
"dedent": "^0.7.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"envinfo": "^7.8.1",
|
||||
"execa": "^5.1.1",
|
||||
"figlet": "^1.5.2",
|
||||
"fs-extra": "^10.1.0",
|
||||
"globby": "^11.1.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"handlebars": "^4.7.7",
|
||||
"inquirer": "^8.2.5",
|
||||
"inquirer-autocomplete-prompt": "^2.0.0",
|
||||
"jscodeshift": "0.15.2",
|
||||
"marked": "^4.3.0",
|
||||
"marked-terminal": "^6.0.0",
|
||||
"node-emoji": "^2.1.3",
|
||||
"node-env-type": "^0.0.8",
|
||||
"node-fetch": "^2.6.7",
|
||||
"ora": "^5.4.1",
|
||||
"pluralize": "^8.0.0",
|
||||
"preferred-pm": "^3.1.3",
|
||||
"prettier": "^2.7.1",
|
||||
"semver": "7.5.2",
|
||||
"semver-diff": "^3.1.1",
|
||||
"temp": "^0.9.4",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-resolve": "^0.1.4",
|
||||
"@types/center-align": "^1.0.0",
|
||||
"@types/dedent": "^0.7.0",
|
||||
"@types/envinfo": "^7.8.1",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/figlet": "^1.5.5",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/inquirer": "^8.2.5",
|
||||
"@types/inquirer-autocomplete-prompt": "^2.0.0",
|
||||
"@types/jest": "^29.2.4",
|
||||
"@types/jscodeshift": "^0.11.11",
|
||||
"@types/marked": "^5.0.1",
|
||||
"@types/marked-terminal": "^3.1.5",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@types/npmcli__package-json": "^4.0.4",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/prettier": "^2.7.3",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/temp": "^0.9.1",
|
||||
"jest": "^29.3.1",
|
||||
"ts-jest": "^29.1.2",
|
||||
"tsup": "^6.7.0",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
75
packages/cli/src/cli.ts
Normal file
75
packages/cli/src/cli.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/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 { 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);
|
||||
devtools(program);
|
||||
add(program);
|
||||
|
||||
program.hook("preAction", async (thisCommand) => {
|
||||
if (thisCommand.args.includes("dev")) {
|
||||
await 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();
|
||||
20
packages/cli/src/commands/add/index.spec.ts
Normal file
20
packages/cli/src/commands/add/index.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ProjectTypes } from "@definitions/projectTypes";
|
||||
import * as utilsProject from "../../utils/project/index";
|
||||
import * as utilsResource from "../../utils/resource/index";
|
||||
import { getDefaultPath } from "./sub-commands/provider/create-providers";
|
||||
|
||||
describe("add", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
42
packages/cli/src/commands/add/index.ts
Normal file
42
packages/cli/src/commands/add/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Command } from "commander";
|
||||
|
||||
import { addCommandPrompt } from "./prompt";
|
||||
import { IntegrationCommand } from "./sub-commands/integration/command";
|
||||
import { availableIntegrations } from "./sub-commands/integration/packages";
|
||||
import { ProviderCommand } from "./sub-commands/provider/command";
|
||||
import { createProviders } from "./sub-commands/provider/create-providers";
|
||||
import { ResourceCommand } from "./sub-commands/resource/command";
|
||||
import { createResources } from "./sub-commands/resource/create-resources";
|
||||
|
||||
const load = (program: Command) => {
|
||||
return program
|
||||
.command("add")
|
||||
.description("Add new resources, providers, or integrations")
|
||||
.allowExcessArguments(false)
|
||||
.action(addCommandAction)
|
||||
.addCommand(ResourceCommand)
|
||||
.addCommand(ProviderCommand)
|
||||
.addCommand(IntegrationCommand);
|
||||
};
|
||||
|
||||
const addCommandAction = async () => {
|
||||
const { component } = await addCommandPrompt();
|
||||
|
||||
if (component.group === "provider") {
|
||||
createProviders([component.component]);
|
||||
}
|
||||
|
||||
if (component.group === "integration") {
|
||||
const integration = availableIntegrations.find(
|
||||
(integration) => integration.id === component.component,
|
||||
);
|
||||
|
||||
await integration?.runTransformer();
|
||||
}
|
||||
|
||||
if (component.group === "resource") {
|
||||
await createResources({}, []);
|
||||
}
|
||||
};
|
||||
|
||||
export default load;
|
||||
57
packages/cli/src/commands/add/prompt.ts
Normal file
57
packages/cli/src/commands/add/prompt.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import chalk from "chalk";
|
||||
import inquirer, { type ListChoiceOptions } from "inquirer";
|
||||
|
||||
import type { IntegrationId } from "./sub-commands/integration/packages";
|
||||
import { buildIntegrationChoices } from "./sub-commands/integration/prompt";
|
||||
import { buildProviderChoices } from "./sub-commands/provider/prompt";
|
||||
import type { ProviderId } from "./sub-commands/provider/providers";
|
||||
|
||||
const wrapChoices = (
|
||||
group: string,
|
||||
choices: ListChoiceOptions[],
|
||||
): ListChoiceOptions<AddCommandComponentAnswer>[] => {
|
||||
return choices.map((choice) => {
|
||||
return {
|
||||
...choice,
|
||||
name: ` . ${choice.name}`,
|
||||
value: {
|
||||
group,
|
||||
component: choice.value,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
type AddCommandComponentAnswer =
|
||||
| { group: "provider"; component: ProviderId }
|
||||
| { group: "integration"; component: IntegrationId }
|
||||
| { group: "resource"; component: "resource" };
|
||||
|
||||
interface AddCommandAnswer {
|
||||
component: AddCommandComponentAnswer;
|
||||
}
|
||||
|
||||
export const addCommandPrompt = async () => {
|
||||
return await inquirer.prompt<AddCommandAnswer>([
|
||||
{
|
||||
type: "list",
|
||||
name: "component",
|
||||
message: "What do you want to add?",
|
||||
choices: [
|
||||
new inquirer.Separator(chalk.bold("Provider")),
|
||||
...wrapChoices("provider", buildProviderChoices()),
|
||||
new inquirer.Separator(chalk.bold("Integration")),
|
||||
...wrapChoices("integration", buildIntegrationChoices()),
|
||||
new inquirer.Separator(chalk.bold("Resource")),
|
||||
{
|
||||
name: chalk.blueBright(" . Add new resource"),
|
||||
value: {
|
||||
group: "resource",
|
||||
component: "resource",
|
||||
},
|
||||
},
|
||||
],
|
||||
pageSize: 25,
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Argument, Command } from "commander";
|
||||
import { availableIntegrations } from "./packages";
|
||||
import { addIntegrationPrompt } from "./prompt";
|
||||
|
||||
const addIntegrationAction = async (name?: string) => {
|
||||
let integrationID = name;
|
||||
|
||||
if (!integrationID) {
|
||||
const answer = await addIntegrationPrompt();
|
||||
|
||||
integrationID = answer.id;
|
||||
}
|
||||
|
||||
const integration = availableIntegrations.find(
|
||||
(integration) => integration.id === integrationID,
|
||||
);
|
||||
|
||||
if (integration) {
|
||||
await integration.runTransformer();
|
||||
}
|
||||
};
|
||||
|
||||
export const IntegrationCommand = new Command("integration")
|
||||
.addArgument(
|
||||
new Argument("[name]", "Name of the integration").choices(
|
||||
availableIntegrations.map((integration) => integration.id),
|
||||
),
|
||||
)
|
||||
.action(addIntegrationAction);
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ProjectTypes } from "@definitions/projectTypes";
|
||||
|
||||
import type { Integration } from ".";
|
||||
import { runTransformer } from "../run-transformer";
|
||||
import { prettifyChoice } from "../utils/prettify-choice";
|
||||
|
||||
const id = "ant-design";
|
||||
const name = "Ant Design";
|
||||
const incompatiblePackages = ["@remix-run/react", "next"];
|
||||
const requiredPackages = ["antd", "@refinedev/antd"];
|
||||
const transformerFileName = "ant-design";
|
||||
|
||||
export const AntDesignIntegration: Integration = {
|
||||
id,
|
||||
getChoice: (projectType: ProjectTypes) => {
|
||||
const title = "Ant Design";
|
||||
const description = "Setup Ant Design with Refine";
|
||||
let disabled;
|
||||
|
||||
if (
|
||||
[
|
||||
ProjectTypes.NEXTJS,
|
||||
ProjectTypes.REMIX,
|
||||
ProjectTypes.REMIX_VITE,
|
||||
ProjectTypes.REMIX_SPA,
|
||||
].includes(projectType)
|
||||
) {
|
||||
disabled =
|
||||
"Automatic setup only available Vite for now. See the documentation for manual installation: https://refine.dev/docs/ui-integrations/ant-design/introduction/#installation";
|
||||
}
|
||||
|
||||
return prettifyChoice({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
disabled,
|
||||
});
|
||||
},
|
||||
runTransformer: async () => {
|
||||
return await runTransformer({
|
||||
incompatiblePackages,
|
||||
integrationName: name,
|
||||
requiredPackages,
|
||||
transformerFileName,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ProjectTypes } from "@definitions/projectTypes";
|
||||
import { AntDesignIntegration } from "./ant-design";
|
||||
import { ReactRouterIntegration } from "./react-router";
|
||||
|
||||
export type IntegrationId = "ant-design" | "react-router";
|
||||
|
||||
export interface Integration {
|
||||
id: IntegrationId;
|
||||
getChoice: (projectType: ProjectTypes) => {
|
||||
name: string;
|
||||
value: IntegrationId;
|
||||
disabled?: string;
|
||||
};
|
||||
runTransformer: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const availableIntegrations: Integration[] = [
|
||||
AntDesignIntegration,
|
||||
ReactRouterIntegration,
|
||||
];
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ProjectTypes } from "@definitions/projectTypes";
|
||||
|
||||
import type { Integration } from ".";
|
||||
import { runTransformer } from "../run-transformer";
|
||||
import { prettifyChoice } from "../utils/prettify-choice";
|
||||
|
||||
const id = "react-router";
|
||||
const name = "React Router";
|
||||
const incompatiblePackages = ["@remix-run/react", "next"];
|
||||
const requiredPackages = ["react-router-dom", "@refinedev/react-router-v6"];
|
||||
const transformerFileName = "react-router";
|
||||
|
||||
export const ReactRouterIntegration: Integration = {
|
||||
id,
|
||||
getChoice: (projectType: ProjectTypes) => {
|
||||
const title = "React Router";
|
||||
const description = "Setup routing with React Router";
|
||||
let disabled;
|
||||
|
||||
if (projectType === ProjectTypes.NEXTJS) {
|
||||
disabled = `Can't be used with Next.js. https://nextjs.org/docs/app/building-your-application/routing`;
|
||||
}
|
||||
|
||||
if (
|
||||
projectType === ProjectTypes.REMIX ||
|
||||
projectType === ProjectTypes.REMIX_VITE ||
|
||||
projectType === ProjectTypes.REMIX_SPA
|
||||
) {
|
||||
disabled = `Can't be used with Remix. https://remix.run/docs/en/main/discussion/routes`;
|
||||
}
|
||||
|
||||
return prettifyChoice({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
disabled,
|
||||
});
|
||||
},
|
||||
runTransformer: async () => {
|
||||
return await runTransformer({
|
||||
incompatiblePackages,
|
||||
integrationName: name,
|
||||
requiredPackages,
|
||||
transformerFileName,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import inquirer from "inquirer";
|
||||
|
||||
import { getProjectType } from "@utils/project";
|
||||
import { availableIntegrations, type IntegrationId } from "./packages";
|
||||
|
||||
export const buildIntegrationChoices = () => {
|
||||
const projectType = getProjectType();
|
||||
|
||||
const integrationChoices = availableIntegrations.map((integration) =>
|
||||
integration.getChoice(projectType),
|
||||
);
|
||||
|
||||
if (integrationChoices.every((choice) => choice.disabled)) {
|
||||
return [
|
||||
{
|
||||
value: "none",
|
||||
name: "No integration available for this project type.",
|
||||
},
|
||||
...integrationChoices,
|
||||
];
|
||||
}
|
||||
return integrationChoices;
|
||||
};
|
||||
|
||||
export const addIntegrationPrompt = async () => {
|
||||
return await inquirer.prompt<{ id: IntegrationId }>([
|
||||
{
|
||||
type: "list",
|
||||
name: "id",
|
||||
message: "Which integration do you want to add?",
|
||||
choices: buildIntegrationChoices(),
|
||||
default: "none",
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { hasIncomatiblePackages, installMissingPackages } from "@utils/package";
|
||||
import execa from "execa";
|
||||
|
||||
interface RunTransformerParams {
|
||||
incompatiblePackages: string[];
|
||||
requiredPackages: string[];
|
||||
integrationName: string;
|
||||
transformerFileName: string;
|
||||
}
|
||||
|
||||
export const runTransformer = async (params: RunTransformerParams) => {
|
||||
const {
|
||||
incompatiblePackages,
|
||||
integrationName,
|
||||
requiredPackages,
|
||||
transformerFileName,
|
||||
} = params;
|
||||
if (hasIncomatiblePackages(incompatiblePackages)) return;
|
||||
|
||||
await installMissingPackages(requiredPackages);
|
||||
|
||||
console.log(`🚀 Setting up ${integrationName}...`);
|
||||
|
||||
const jscodeshiftExecutable = require.resolve(".bin/jscodeshift");
|
||||
const { stderr } = execa.sync(jscodeshiftExecutable, [
|
||||
"./",
|
||||
"--extensions=ts,tsx,js,jsx",
|
||||
"--parser=tsx",
|
||||
`--transform=${__dirname}/../src/transformers/integrations/${transformerFileName}.ts`,
|
||||
"--ignore-pattern=.cache",
|
||||
"--ignore-pattern=node_modules",
|
||||
"--ignore-pattern=build",
|
||||
"--ignore-pattern=.next",
|
||||
"--ignore-pattern=dist",
|
||||
]);
|
||||
|
||||
if (stderr) {
|
||||
console.log(stderr);
|
||||
}
|
||||
|
||||
console.log(`🎉 ${integrationName} setup completed!`);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import chalk from "chalk";
|
||||
import type { IntegrationId } from "../packages";
|
||||
|
||||
interface IntegrationChoice {
|
||||
id: IntegrationId;
|
||||
title: string;
|
||||
description: string;
|
||||
disabled?: string;
|
||||
}
|
||||
|
||||
export const prettifyChoice = (choice: IntegrationChoice) => {
|
||||
const { id, title, description, disabled } = choice;
|
||||
|
||||
if (disabled) {
|
||||
return {
|
||||
value: id,
|
||||
name: `${chalk.gray(title)}`,
|
||||
disabled: chalk.redBright(disabled),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: id,
|
||||
name: `${chalk.blueBright(title)} - ${description}`,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Argument, Command } from "commander";
|
||||
|
||||
import { createProviders } from "./create-providers";
|
||||
import { addProviderPrompt } from "./prompt";
|
||||
import { availableProviders, type ProviderId } from "./providers";
|
||||
|
||||
export const createProviderAction = async (
|
||||
providers: ProviderId[],
|
||||
options: { path?: string },
|
||||
) => {
|
||||
if (!providers.length) {
|
||||
const { providers, providersPath } = await addProviderPrompt();
|
||||
|
||||
return createProviders(providers, providersPath);
|
||||
}
|
||||
|
||||
createProviders(providers, options.path);
|
||||
};
|
||||
|
||||
export const ProviderCommand = new Command("provider")
|
||||
.addArgument(
|
||||
new Argument("[providers...]", "Create provider(s)")
|
||||
.choices(availableProviders.map((provider) => provider.id))
|
||||
.default([]),
|
||||
)
|
||||
.option("-p, --path [path]", "Path to generate providers")
|
||||
.action(createProviderAction);
|
||||
@@ -0,0 +1,104 @@
|
||||
import { getProjectType } from "@utils/project";
|
||||
import { getFilesPathByProject, getProviderPath } from "@utils/resource";
|
||||
import {
|
||||
copySync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
pathExistsSync,
|
||||
readdirSync,
|
||||
} from "fs-extra";
|
||||
import { join, relative } from "path";
|
||||
import {
|
||||
availableProviders,
|
||||
type Provider,
|
||||
type ProviderId,
|
||||
} from "./providers";
|
||||
|
||||
const baseTemplatePath = `${__dirname}/../templates/provider`;
|
||||
const baseAssetsPath = `${__dirname}/../templates/assets`;
|
||||
|
||||
export const createProviders = (
|
||||
providerIds: ProviderId[],
|
||||
pathFromArgs?: string,
|
||||
) => {
|
||||
providerIds.forEach((providerId) => {
|
||||
const { fileName, templateFileName } = getProviderOptions(providerId);
|
||||
const projectFilesBasePath = getFilesPathByProject(getProjectType());
|
||||
const folderPath = pathFromArgs ?? getDefaultPath();
|
||||
const filePath = join(folderPath, fileName);
|
||||
const fullPath = join(process.cwd(), folderPath, fileName);
|
||||
const projectFilesPath = join(process.cwd(), projectFilesBasePath);
|
||||
|
||||
if (pathExistsSync(fullPath)) {
|
||||
console.error(`❌ Provider (${filePath}) already exist!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// create destination dir
|
||||
mkdirSync(folderPath, { recursive: true });
|
||||
|
||||
// copy template file to destination
|
||||
copySync(`${baseTemplatePath}/${templateFileName}`, fullPath);
|
||||
|
||||
console.log(`🎉 Provider (${filePath}) created successfully!`);
|
||||
|
||||
const copiedAssets: string[] = [];
|
||||
const failedAssets: string[] = [];
|
||||
|
||||
if (pathExistsSync(`${baseAssetsPath}/${providerId}`)) {
|
||||
try {
|
||||
readdirSync(`${baseAssetsPath}/${providerId}`, {
|
||||
recursive: true,
|
||||
withFileTypes: true,
|
||||
}).forEach((file) => {
|
||||
if (!file.isDirectory()) {
|
||||
const fromPath = join(file.path, file.name);
|
||||
const destinationPath = join(
|
||||
projectFilesPath,
|
||||
relative(join(baseAssetsPath, providerId), file.path),
|
||||
file.name,
|
||||
);
|
||||
const relativeDestinationPath = join(
|
||||
projectFilesBasePath,
|
||||
relative(projectFilesPath, destinationPath),
|
||||
);
|
||||
|
||||
if (existsSync(destinationPath)) {
|
||||
failedAssets.push(relativeDestinationPath);
|
||||
} else {
|
||||
try {
|
||||
copySync(fromPath, destinationPath);
|
||||
copiedAssets.push(relativeDestinationPath);
|
||||
} catch (error) {
|
||||
failedAssets.push(relativeDestinationPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
if (copiedAssets.length) {
|
||||
console.log(`🎉 Created additional assets: ${copiedAssets.join(", ")}`);
|
||||
}
|
||||
if (failedAssets.length) {
|
||||
console.error(`❌ Failed to create assets: ${failedAssets.join(", ")}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getProviderOptions = (providerId: ProviderId): Provider => {
|
||||
const provider = availableProviders.find((p) => p.id === providerId);
|
||||
|
||||
if (!provider) {
|
||||
throw new Error(`Invalid provider: ${providerId}`);
|
||||
}
|
||||
|
||||
return provider;
|
||||
};
|
||||
|
||||
export const getDefaultPath = () => {
|
||||
const projectType = getProjectType();
|
||||
const { path } = getProviderPath(projectType);
|
||||
return path;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import chalk from "chalk";
|
||||
import inquirer from "inquirer";
|
||||
|
||||
import { getDefaultPath } from "./create-providers";
|
||||
import { availableProviders, type ProviderId } from "./providers";
|
||||
|
||||
export const buildProviderChoices = () => {
|
||||
return availableProviders.map((provider) => {
|
||||
const { id, title, description } = provider;
|
||||
|
||||
return {
|
||||
value: id,
|
||||
name: `${chalk.blueBright(title)} - ${description}`,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const addProviderPrompt = async () => {
|
||||
return await inquirer.prompt<{
|
||||
providers: ProviderId[];
|
||||
providersPath: string;
|
||||
}>([
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "providers",
|
||||
message: "Which providers do you want to add?",
|
||||
choices: buildProviderChoices(),
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "providersPath",
|
||||
message: "Where do you want to generate the providers?",
|
||||
default: getDefaultPath(),
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
export type ProviderId =
|
||||
| "auth"
|
||||
| "live"
|
||||
| "data"
|
||||
| "access-control"
|
||||
| "notification"
|
||||
| "i18n"
|
||||
| "audit-log";
|
||||
|
||||
export interface Provider {
|
||||
id: ProviderId;
|
||||
title: string;
|
||||
description: string;
|
||||
fileName: string;
|
||||
templateFileName: string;
|
||||
}
|
||||
|
||||
export const availableProviders: Provider[] = [
|
||||
{
|
||||
id: "auth",
|
||||
title: "Auth provider",
|
||||
description: "Manage user authentication and authorization",
|
||||
fileName: "auth-provider.tsx",
|
||||
templateFileName: "demo-auth-provider.tsx.template",
|
||||
},
|
||||
{
|
||||
id: "live",
|
||||
title: "Live provider",
|
||||
description: "Enable real-time updates and synchronization",
|
||||
fileName: "live-provider.tsx",
|
||||
templateFileName: "demo-live-provider.tsx.template",
|
||||
},
|
||||
{
|
||||
id: "data",
|
||||
title: "Data provider",
|
||||
description: "Communicate with your API",
|
||||
fileName: "data-provider.tsx",
|
||||
templateFileName: "demo-data-provider.tsx.template",
|
||||
},
|
||||
{
|
||||
id: "access-control",
|
||||
title: "Access Control",
|
||||
description: "Manage user permissions & roles",
|
||||
fileName: "access-control-provider.tsx",
|
||||
templateFileName: "demo-access-control-provider.tsx.template",
|
||||
},
|
||||
{
|
||||
id: "notification",
|
||||
title: "Notification provider",
|
||||
description: "Display in-app alerts and messages",
|
||||
fileName: "notification-provider.tsx",
|
||||
templateFileName: "demo-notification-provider.tsx.template",
|
||||
},
|
||||
{
|
||||
id: "i18n",
|
||||
title: "I18n provider",
|
||||
description: "Support multiple languages and locales",
|
||||
fileName: "i18n-provider.tsx",
|
||||
templateFileName: "demo-i18n-provider.tsx.template",
|
||||
},
|
||||
{
|
||||
id: "audit-log",
|
||||
title: "Audit Log provider",
|
||||
description: "Display audit logs for your resources",
|
||||
fileName: "audit-log-provider.tsx",
|
||||
templateFileName: "demo-audit-log-provider.tsx.template",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Argument, Command } from "commander";
|
||||
import { createResources } from "./create-resources";
|
||||
|
||||
const createResourceAction = async (
|
||||
resources: string[],
|
||||
options: { actions: string },
|
||||
command: Command,
|
||||
) => {
|
||||
createResources(
|
||||
{
|
||||
actions: options?.actions,
|
||||
path: command.optsWithGlobals().path,
|
||||
},
|
||||
resources,
|
||||
);
|
||||
};
|
||||
|
||||
export const ResourceCommand = new Command("resource")
|
||||
.addArgument(new Argument("[resources...]", "Create new resource(s)"))
|
||||
.option("-p, --path [path]", "Path to generated resource files")
|
||||
.option(
|
||||
"-a, --actions [actions]",
|
||||
"Only generate the specified resource actions. (ex: list,create,edit,show)",
|
||||
"list,create,edit,show",
|
||||
)
|
||||
.action(createResourceAction);
|
||||
@@ -0,0 +1,249 @@
|
||||
import { ProjectTypes } from "@definitions/projectTypes";
|
||||
import { compileDir } from "@utils/compile";
|
||||
import { installPackages, isInstalled } from "@utils/package";
|
||||
import { getProjectType, getUIFramework } from "@utils/project";
|
||||
import { getResourcePath } from "@utils/resource";
|
||||
import spinner from "@utils/spinner";
|
||||
import { uppercaseFirstChar } from "@utils/text";
|
||||
import execa from "execa";
|
||||
import {
|
||||
copySync,
|
||||
mkdirSync,
|
||||
moveSync,
|
||||
pathExistsSync,
|
||||
unlinkSync,
|
||||
} from "fs-extra";
|
||||
import inquirer from "inquirer";
|
||||
import { join } from "path";
|
||||
import { plural } from "pluralize";
|
||||
import temp from "temp";
|
||||
|
||||
export 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) {
|
||||
const { name, selectedActions } = await inquirer.prompt<{
|
||||
name: string;
|
||||
selectedActions: string[];
|
||||
}>([
|
||||
{
|
||||
type: "input",
|
||||
name: "name",
|
||||
message: "Resource Name (users, products, orders etc.)",
|
||||
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(",");
|
||||
}
|
||||
|
||||
let isAtleastOneResourceCreated = false;
|
||||
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;
|
||||
}
|
||||
isAtleastOneResourceCreated = true;
|
||||
|
||||
// uppercase first letter
|
||||
const resource = uppercaseFirstChar(resourceName);
|
||||
|
||||
// get the project type
|
||||
const uiFramework = getUIFramework();
|
||||
|
||||
// if next.js, need to add the use client directive
|
||||
const projectType = getProjectType();
|
||||
const isNextJs = projectType === ProjectTypes.NEXTJS;
|
||||
|
||||
const sourceDir = `${getCommandRootDir()}/../templates/resource/components`;
|
||||
|
||||
// create temp dir
|
||||
const tempDir = generateTempDir();
|
||||
|
||||
// copy template files
|
||||
copySync(sourceDir, tempDir);
|
||||
|
||||
const compileParams = {
|
||||
resourceName,
|
||||
resource,
|
||||
actions: customActions || defaultActions,
|
||||
uiFramework,
|
||||
isNextJs,
|
||||
};
|
||||
|
||||
// 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();
|
||||
|
||||
// if use Next.js, generate page files. This makes easier to use the resource
|
||||
if (isNextJs) {
|
||||
generateNextJsPages(
|
||||
resource,
|
||||
resourceFolderName,
|
||||
customActions || defaultActions,
|
||||
);
|
||||
}
|
||||
|
||||
const jscodeshiftExecutable = require.resolve(".bin/jscodeshift");
|
||||
const { stderr } = execa.sync(jscodeshiftExecutable, [
|
||||
"./",
|
||||
"--extensions=ts,tsx,js,jsx",
|
||||
"--parser=tsx",
|
||||
`--transform=${getCommandRootDir()}/../src/transformers/resource.ts`,
|
||||
"--ignore-pattern=.cache",
|
||||
"--ignore-pattern=node_modules",
|
||||
"--ignore-pattern=build",
|
||||
"--ignore-pattern=.next",
|
||||
"--ignore-pattern=dist",
|
||||
// pass custom params to transformer file
|
||||
`--__actions=${compileParams.actions}`,
|
||||
`--__pathAlias=${getResourcePath(getProjectType()).alias}`,
|
||||
`--__resourceFolderName=${resourceFolderName}`,
|
||||
`--__resource=${resource}`,
|
||||
`--__resourceName=${resourceName}`,
|
||||
]);
|
||||
|
||||
if (stderr) {
|
||||
console.log(stderr);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`🎉 Resource (${destinationResourcePath}) generated successfully!`,
|
||||
);
|
||||
});
|
||||
|
||||
if (isAtleastOneResourceCreated) {
|
||||
installInferencer();
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
// this export is for testing
|
||||
export const getCommandRootDir = () => {
|
||||
return __dirname;
|
||||
};
|
||||
|
||||
const generateTempDir = (): string => {
|
||||
temp.track();
|
||||
return temp.mkdirSync("resource");
|
||||
};
|
||||
|
||||
/**
|
||||
* generate resource pages for Next.js App Router
|
||||
*/
|
||||
const generateNextJsPages = (
|
||||
resource: string,
|
||||
resourceFolderName: string,
|
||||
actions: string[],
|
||||
): void => {
|
||||
const resourcePageRootDirPath = join(
|
||||
process.cwd(),
|
||||
"src/app/",
|
||||
resourceFolderName,
|
||||
);
|
||||
|
||||
// this is specific to Next.js, so defined here
|
||||
const actionPageRelativeDirPaths: { [key: string]: string } = {
|
||||
list: "/",
|
||||
create: "/create",
|
||||
edit: "/edit/[id]",
|
||||
show: "/show/[id]",
|
||||
};
|
||||
|
||||
actions.forEach((action) => {
|
||||
// create page dir
|
||||
const actionPageRelativeDirPath = actionPageRelativeDirPaths[action];
|
||||
const actionPageDirPath = join(
|
||||
resourcePageRootDirPath,
|
||||
actionPageRelativeDirPath,
|
||||
);
|
||||
mkdirSync(actionPageDirPath, { recursive: true });
|
||||
|
||||
// copy template files as page files
|
||||
const sourceFilePath = `${getCommandRootDir()}/../templates/resource/pages/next/${actionPageRelativeDirPath}/page.tsx.hbs`;
|
||||
const destFilePath = join(actionPageDirPath, "page.tsx.hbs");
|
||||
copySync(sourceFilePath, destFilePath);
|
||||
});
|
||||
|
||||
// compile page files
|
||||
const compileParams = { resource, resourceFolderName };
|
||||
compileDir(resourcePageRootDirPath, compileParams);
|
||||
};
|
||||
|
||||
export const installInferencer = async () => {
|
||||
console.log();
|
||||
const isInferencerInstalled = await spinner(
|
||||
() => isInstalled("@refinedev/inferencer"),
|
||||
"Checking if '@refinedev/inferencer' package is installed...",
|
||||
);
|
||||
if (!isInferencerInstalled) {
|
||||
console.log("📦 Installing '@refinedev/inferencer' package...");
|
||||
await installPackages(
|
||||
["@refinedev/inferencer@latest"],
|
||||
"add",
|
||||
"✅ '@refinedev/inferencer' package installed successfully!",
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
import { ProjectTypes } from "@definitions/projectTypes";
|
||||
import * as utilsProject from "../../../../utils/project/index";
|
||||
import * as testTargetModule from "@commands/add/sub-commands/resource/create-resources";
|
||||
import { existsSync, readFileSync, rmdirSync } from "fs-extra";
|
||||
|
||||
const srcDirPath = `${__dirname}/../../../..`;
|
||||
|
||||
describe("add", () => {
|
||||
beforeAll(() => {
|
||||
// useful for speed up the tests.
|
||||
jest.spyOn(console, "log").mockImplementation();
|
||||
|
||||
jest.spyOn(testTargetModule, "installInferencer").mockImplementation();
|
||||
});
|
||||
|
||||
it("should generate next js pages", () => {
|
||||
jest
|
||||
.spyOn(utilsProject, "getProjectType")
|
||||
.mockReturnValue(ProjectTypes.NEXTJS);
|
||||
|
||||
// if the execution path is left as it is, it will be "src\commands\add\sub-commands\resource",
|
||||
// so you will not be able to find the template directory unless you move it up.
|
||||
jest
|
||||
.spyOn(testTargetModule, "getCommandRootDir")
|
||||
.mockReturnValue(srcDirPath);
|
||||
|
||||
const actions = testTargetModule.defaultActions;
|
||||
testTargetModule.createResources({ actions: actions.join(",") }, ["tmps"]);
|
||||
|
||||
const nextComponentDirPath = `${srcDirPath}/components/tmps`;
|
||||
const nextPageRootDirPath = `${srcDirPath}/app`;
|
||||
|
||||
expect(existsSync(nextComponentDirPath)).toBe(true);
|
||||
expect(existsSync(`${nextPageRootDirPath}/tmps`)).toBe(true);
|
||||
|
||||
// cleanup
|
||||
rmdirSync(nextComponentDirPath, { recursive: true });
|
||||
rmdirSync(nextPageRootDirPath, { recursive: true });
|
||||
});
|
||||
|
||||
it("should include use client in the component if use Next.js", () => {
|
||||
jest
|
||||
.spyOn(utilsProject, "getProjectType")
|
||||
.mockReturnValue(ProjectTypes.NEXTJS);
|
||||
|
||||
jest
|
||||
.spyOn(testTargetModule, "getCommandRootDir")
|
||||
.mockReturnValue(srcDirPath);
|
||||
|
||||
const actions = testTargetModule.defaultActions;
|
||||
testTargetModule.createResources({ actions: actions.join(",") }, ["tmps"]);
|
||||
|
||||
const nextComponentDirPath = `${srcDirPath}/components/tmps`;
|
||||
|
||||
actions.forEach((action) => {
|
||||
const componentFilePath = `${nextComponentDirPath}/${action}.tsx`;
|
||||
const componentContent = readFileSync(componentFilePath, "utf-8");
|
||||
expect(componentContent).toContain("use client");
|
||||
});
|
||||
|
||||
// cleanup
|
||||
const nextPageRootDirPath = `${srcDirPath}/app`;
|
||||
rmdirSync(nextComponentDirPath, { recursive: true });
|
||||
rmdirSync(nextPageRootDirPath, { recursive: true });
|
||||
});
|
||||
|
||||
it("should not include use client in the component if don't use Next.js", () => {
|
||||
jest
|
||||
.spyOn(utilsProject, "getProjectType")
|
||||
.mockReturnValue(ProjectTypes.REACT_SCRIPT);
|
||||
|
||||
jest
|
||||
.spyOn(testTargetModule, "getCommandRootDir")
|
||||
.mockReturnValue(srcDirPath);
|
||||
|
||||
const actions = testTargetModule.defaultActions;
|
||||
testTargetModule.createResources({ actions: actions.join(",") }, ["tmps"]);
|
||||
|
||||
const reactComponentRootDirPath = `${srcDirPath}/pages`;
|
||||
|
||||
actions.forEach((action) => {
|
||||
const componentFilePath = `${reactComponentRootDirPath}/tmps/${action}.tsx`;
|
||||
const componentContent = readFileSync(componentFilePath, "utf-8");
|
||||
expect(componentContent).not.toContain("use client");
|
||||
});
|
||||
|
||||
// cleanup
|
||||
rmdirSync(reactComponentRootDirPath, { recursive: true });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
resourceName: "blog-posts",
|
||||
folderName: "blog-posts",
|
||||
componentNamesByActions: {
|
||||
list: "BlogPostsList",
|
||||
create: "BlogPostsCreate",
|
||||
show: "BlogPostsShow",
|
||||
edit: "BlogPostsEdit",
|
||||
},
|
||||
},
|
||||
{
|
||||
resourceName: "blog-post",
|
||||
folderName: "blog-posts",
|
||||
componentNamesByActions: {
|
||||
list: "BlogPostList",
|
||||
create: "BlogPostCreate",
|
||||
show: "BlogPostShow",
|
||||
edit: "BlogPostEdit",
|
||||
},
|
||||
},
|
||||
{
|
||||
resourceName: "product",
|
||||
folderName: "products",
|
||||
componentNamesByActions: {
|
||||
list: "ProductList",
|
||||
create: "ProductCreate",
|
||||
show: "ProductShow",
|
||||
edit: "ProductEdit",
|
||||
},
|
||||
},
|
||||
{
|
||||
resourceName: "blogPosts",
|
||||
folderName: "blogposts",
|
||||
componentNamesByActions: {
|
||||
list: "BlogPostsList",
|
||||
create: "BlogPostsCreate",
|
||||
show: "BlogPostsShow",
|
||||
edit: "BlogPostsEdit",
|
||||
},
|
||||
},
|
||||
])(
|
||||
"should generate components and folders for '$resourceName'",
|
||||
(testCase) => {
|
||||
jest
|
||||
.spyOn(utilsProject, "getProjectType")
|
||||
.mockReturnValue(ProjectTypes.VITE);
|
||||
|
||||
jest
|
||||
.spyOn(testTargetModule, "getCommandRootDir")
|
||||
.mockReturnValue(srcDirPath);
|
||||
|
||||
const actions = ["list", "create", "show", "edit"] as const;
|
||||
testTargetModule.createResources({ actions: actions.join(",") }, [
|
||||
testCase.resourceName,
|
||||
]);
|
||||
|
||||
const reactComponentRootDirPath = `${srcDirPath}/pages`;
|
||||
|
||||
actions.forEach((action) => {
|
||||
const componentFilePath = `${reactComponentRootDirPath}/${testCase.folderName}/${action}.tsx`;
|
||||
const componentContent = readFileSync(componentFilePath, "utf-8");
|
||||
expect(componentContent).toContain(
|
||||
`const ${testCase.componentNamesByActions[action]} = () => {`,
|
||||
);
|
||||
});
|
||||
|
||||
// cleanup
|
||||
rmdirSync(reactComponentRootDirPath, { recursive: true });
|
||||
},
|
||||
);
|
||||
});
|
||||
239
packages/cli/src/commands/check-updates/index.test.tsx
Normal file
239
packages/cli/src/commands/check-updates/index.test.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import type {
|
||||
NpmOutdatedResponse,
|
||||
RefinePackageInstalledVersionData,
|
||||
} from "@definitions/package";
|
||||
import * as checkUpdates from "./index";
|
||||
import * as packageUtils from "@utils/package";
|
||||
|
||||
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",
|
||||
dependent: "",
|
||||
location: "",
|
||||
},
|
||||
"@refinedev/cli": {
|
||||
current: "1.1.1",
|
||||
wanted: "1.1.1",
|
||||
latest: "1.1.0",
|
||||
dependent: "",
|
||||
location: "",
|
||||
},
|
||||
"@pankod/canvas2video": {
|
||||
current: "1.1.1",
|
||||
wanted: "1.1.1",
|
||||
latest: "1.1.1",
|
||||
dependent: "",
|
||||
location: "",
|
||||
},
|
||||
"@owner/package-name": {
|
||||
current: "1.1.1",
|
||||
wanted: "1.1.1",
|
||||
latest: "1.1.0",
|
||||
dependent: "",
|
||||
location: "",
|
||||
},
|
||||
"@owner/package-name1": {
|
||||
current: "N/A",
|
||||
wanted: "undefined",
|
||||
latest: "NaN",
|
||||
dependent: "",
|
||||
location: "",
|
||||
},
|
||||
"@owner/refine-react": {
|
||||
current: "1.0.0",
|
||||
wanted: "1.0.1",
|
||||
latest: "2.0.0",
|
||||
dependent: "",
|
||||
location: "",
|
||||
},
|
||||
},
|
||||
output: [
|
||||
{
|
||||
name: "@refinedev/core",
|
||||
current: "1.0.0",
|
||||
wanted: "1.0.1",
|
||||
latest: "2.0.0",
|
||||
changelog: "https://c.refine.dev/core",
|
||||
dependent: "",
|
||||
location: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
jest
|
||||
.spyOn(checkUpdates, "getOutdatedPackageList")
|
||||
.mockResolvedValue(testCase.input);
|
||||
|
||||
const result = await getOutdatedRefinePackages();
|
||||
expect(result).toEqual(testCase.output);
|
||||
}
|
||||
});
|
||||
|
||||
describe("getWantedWithPreferredWildcard", () => {
|
||||
it("package not found in package.json", () => {
|
||||
jest.spyOn(packageUtils, "getDependenciesWithVersion").mockReturnValue({});
|
||||
|
||||
const result = checkUpdates.getWantedWithPreferredWildcard(
|
||||
"@refinedev/core",
|
||||
"1.0.1",
|
||||
);
|
||||
expect(result).toEqual("^1.0.1");
|
||||
});
|
||||
|
||||
it("with carret", () => {
|
||||
jest.spyOn(packageUtils, "getDependenciesWithVersion").mockReturnValue({
|
||||
"@refinedev/core": "^1.0.0",
|
||||
});
|
||||
|
||||
const result = checkUpdates.getWantedWithPreferredWildcard(
|
||||
"@refinedev/core",
|
||||
"1.0.1",
|
||||
);
|
||||
expect(result).toEqual("^1.0.1");
|
||||
});
|
||||
|
||||
it("with tilda", () => {
|
||||
jest.spyOn(packageUtils, "getDependenciesWithVersion").mockReturnValue({
|
||||
"@refinedev/core": "~1.0.0",
|
||||
});
|
||||
|
||||
const result = checkUpdates.getWantedWithPreferredWildcard(
|
||||
"@refinedev/core",
|
||||
"1.0.1",
|
||||
);
|
||||
expect(result).toEqual("~1.0.1");
|
||||
});
|
||||
|
||||
it("without caret and tilda", () => {
|
||||
jest.spyOn(packageUtils, "getDependenciesWithVersion").mockReturnValue({
|
||||
"@refinedev/core": "1.0.0",
|
||||
});
|
||||
|
||||
const result = checkUpdates.getWantedWithPreferredWildcard(
|
||||
"@refinedev/core",
|
||||
"1.0.1",
|
||||
);
|
||||
expect(result).toEqual("1.0.1");
|
||||
});
|
||||
|
||||
it("with `.x.x`", () => {
|
||||
jest.spyOn(packageUtils, "getDependenciesWithVersion").mockReturnValue({
|
||||
"@refinedev/core": "1.x.x",
|
||||
});
|
||||
|
||||
const result = checkUpdates.getWantedWithPreferredWildcard(
|
||||
"@refinedev/core",
|
||||
"1.10.1",
|
||||
);
|
||||
expect(result).toEqual("1.x.x");
|
||||
});
|
||||
|
||||
it("with `.x`", () => {
|
||||
jest.spyOn(packageUtils, "getDependenciesWithVersion").mockReturnValue({
|
||||
"@refinedev/core": "1.1.x",
|
||||
});
|
||||
|
||||
const result = checkUpdates.getWantedWithPreferredWildcard(
|
||||
"@refinedev/core",
|
||||
"1.1.10",
|
||||
);
|
||||
expect(result).toEqual("1.1.x");
|
||||
});
|
||||
|
||||
it("with `latest`", () => {
|
||||
jest.spyOn(packageUtils, "getDependenciesWithVersion").mockReturnValue({
|
||||
"@refinedev/core": "latest",
|
||||
});
|
||||
|
||||
const result = checkUpdates.getWantedWithPreferredWildcard(
|
||||
"@refinedev/core",
|
||||
"3.1.1",
|
||||
);
|
||||
expect(result).toEqual("latest");
|
||||
});
|
||||
|
||||
it("with range", () => {
|
||||
jest.spyOn(packageUtils, "getDependenciesWithVersion").mockReturnValue({
|
||||
"@refinedev/core": ">=1.0.0 <=1.1.9",
|
||||
});
|
||||
|
||||
const result = checkUpdates.getWantedWithPreferredWildcard(
|
||||
"@refinedev/core",
|
||||
"1.0.0-rc.10",
|
||||
);
|
||||
expect(result).toEqual(">=1.0.0 <=1.1.9");
|
||||
});
|
||||
|
||||
it("multiple sets", () => {
|
||||
jest.spyOn(packageUtils, "getDependenciesWithVersion").mockReturnValue({
|
||||
"@refinedev/core": "^2 <2.2 || > 2.3",
|
||||
});
|
||||
|
||||
const result = checkUpdates.getWantedWithPreferredWildcard(
|
||||
"@refinedev/core",
|
||||
"2.3.1",
|
||||
);
|
||||
expect(result).toEqual("^2 <2.2 || > 2.3");
|
||||
});
|
||||
|
||||
it("with `*`", () => {
|
||||
jest.spyOn(packageUtils, "getDependenciesWithVersion").mockReturnValue({
|
||||
"@refinedev/core": "*",
|
||||
});
|
||||
|
||||
const result = checkUpdates.getWantedWithPreferredWildcard(
|
||||
"@refinedev/core",
|
||||
"3.1.1",
|
||||
);
|
||||
expect(result).toEqual("*");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLatestMinorVersionOfPackage", () => {
|
||||
it.each([
|
||||
{
|
||||
versionList: [
|
||||
"1.0.0",
|
||||
"1.0.1",
|
||||
"1.0.2",
|
||||
"1.1.0",
|
||||
"1.1.1",
|
||||
"1.1.2",
|
||||
"2.0.0",
|
||||
],
|
||||
currentVersion: "1.1.0",
|
||||
expected: "1.1.2",
|
||||
},
|
||||
{
|
||||
versionList: ["1.0.0", "1.0.1"],
|
||||
currentVersion: "1.0.1",
|
||||
expected: "1.0.1",
|
||||
},
|
||||
{
|
||||
versionList: [],
|
||||
currentVersion: "1.0.1",
|
||||
expected: "1.0.1",
|
||||
},
|
||||
])("should return %p", async ({ versionList, currentVersion, expected }) => {
|
||||
jest
|
||||
.spyOn(packageUtils, "getAllVersionsOfPackage")
|
||||
.mockResolvedValueOnce(versionList);
|
||||
const result = await checkUpdates.getLatestMinorVersionOfPackage(
|
||||
"@refinedev/core",
|
||||
currentVersion,
|
||||
);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
177
packages/cli/src/commands/check-updates/index.tsx
Normal file
177
packages/cli/src/commands/check-updates/index.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import type { Command } from "commander";
|
||||
import { printUpdateWarningTable } from "@components/update-warning-table";
|
||||
import {
|
||||
getAllVersionsOfPackage,
|
||||
getDependenciesWithVersion,
|
||||
pmCommands,
|
||||
} from "@utils/package";
|
||||
import execa from "execa";
|
||||
import spinner from "@utils/spinner";
|
||||
import type {
|
||||
NpmOutdatedResponse,
|
||||
RefinePackageInstalledVersionData,
|
||||
} from "@definitions/package";
|
||||
import semverDiff from "semver-diff";
|
||||
import { maxSatisfying } from "semver";
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Uses `npm outdated` command to get the list of outdated packages.
|
||||
* @returns `[]` if no Refine package found.
|
||||
* @returns `Refine` packages that have updates.
|
||||
*/
|
||||
export const getOutdatedRefinePackages = async () => {
|
||||
const packages = await getOutdatedPackageList();
|
||||
if (!packages) return [];
|
||||
|
||||
const list: RefinePackageInstalledVersionData[] = [];
|
||||
|
||||
Object.keys(packages).forEach((packageName) => {
|
||||
const dependency = packages[packageName];
|
||||
|
||||
if (packageName.includes("@refinedev")) {
|
||||
list.push({
|
||||
...dependency,
|
||||
name: packageName,
|
||||
changelog: packageName.replace(/@refinedev\//, "https://c.refine.dev/"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns `npm outdated` command response
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* The `npm outdated` command's `wanted` field shows the desired update version (e.g., `^1.2.0` in `package.json` resolves to `1.2.1`).
|
||||
* This function returns the version that matches the semver range in `package.json` (e.g., `^1.2.0` resolves to `^1.2.1`).
|
||||
*
|
||||
* @param packageName The name of the package.
|
||||
* @param versionWanted The version that the user wants to update to. Wihtout semver range.
|
||||
* @returns The version that satisfies the semver range in `package.json` with the preferred wildcard.
|
||||
*/
|
||||
export const getWantedWithPreferredWildcard = (
|
||||
packageName: RefinePackageInstalledVersionData["name"],
|
||||
versionWanted: RefinePackageInstalledVersionData["wanted"],
|
||||
): string => {
|
||||
const dependencies = getDependenciesWithVersion();
|
||||
const versionInPackageJson = dependencies[packageName];
|
||||
|
||||
if (!versionInPackageJson) {
|
||||
return `^${versionWanted}`;
|
||||
}
|
||||
|
||||
if (versionInPackageJson === "latest") {
|
||||
return "latest";
|
||||
}
|
||||
|
||||
if (versionInPackageJson === "*") {
|
||||
return "*";
|
||||
}
|
||||
|
||||
// has range
|
||||
// if the version in the package.json has a range, it means the user has installed the package with a range.
|
||||
// in that case, we should not change the version. package manager will install the latest version that satisfies the semver range.
|
||||
if (
|
||||
[">", "<", ">=", "<=", "||"].some((char) =>
|
||||
versionInPackageJson.includes(char),
|
||||
)
|
||||
) {
|
||||
return versionInPackageJson;
|
||||
}
|
||||
|
||||
// has `x`
|
||||
// if the version in the package.json has `x` in it, it means the user has installed the package with a wildcard.
|
||||
// in that case, we should not change the version. package manager will install the latest version that satisfies the semver range.
|
||||
if (versionInPackageJson?.includes("x")) {
|
||||
return `${versionInPackageJson}`;
|
||||
}
|
||||
|
||||
// has tilda
|
||||
if (versionInPackageJson?.startsWith("~")) {
|
||||
return `~${versionWanted}`;
|
||||
}
|
||||
|
||||
// has caret
|
||||
if (versionInPackageJson?.startsWith("^")) {
|
||||
return `^${versionWanted}`;
|
||||
}
|
||||
|
||||
return versionWanted;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param packageName to get the latest minor version of the package available on npm.
|
||||
* @param version current installed version of the package. This will be used to calculate the latest minor version.
|
||||
* @returns The latest minor version of the package available on npm.
|
||||
*/
|
||||
export const getLatestMinorVersionOfPackage = async (
|
||||
packageName: RefinePackageInstalledVersionData["name"],
|
||||
version: RefinePackageInstalledVersionData["wanted"],
|
||||
) => {
|
||||
const versionAll = await getAllVersionsOfPackage(packageName);
|
||||
|
||||
/**
|
||||
* The `semver` package's `maxSatisfying` function returns the highest version in the list that satisfies the range.
|
||||
*/
|
||||
const versionLatest = maxSatisfying(versionAll, `^${version}`);
|
||||
return versionLatest ?? version;
|
||||
};
|
||||
|
||||
export default load;
|
||||
32
packages/cli/src/commands/create-resource/index.ts
Normal file
32
packages/cli/src/commands/create-resource/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Command } from "commander";
|
||||
import { getProjectType } from "@utils/project";
|
||||
import { getResourcePath } from "@utils/resource";
|
||||
import { createResources } from "@commands/add/sub-commands/resource/create-resources";
|
||||
|
||||
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;
|
||||
232
packages/cli/src/commands/devtools/index.ts
Normal file
232
packages/cli/src/commands/devtools/index.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
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, type 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 Devtools server; it starts on port 5001.",
|
||||
)
|
||||
.addArgument(
|
||||
new Argument("[command]", "devtools related commands")
|
||||
.choices(commands)
|
||||
.default(defaultCommand),
|
||||
)
|
||||
.addHelpText(
|
||||
"after",
|
||||
`
|
||||
Commands:
|
||||
start Start Refine Devtools server
|
||||
init Install Refine 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 ({
|
||||
exitOnError = true,
|
||||
}: { exitOnError?: boolean } = {}) => {
|
||||
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({
|
||||
onError: () => {
|
||||
if (exitOnError) {
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
}).catch((e) => {});
|
||||
};
|
||||
|
||||
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;
|
||||
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;
|
||||
66
packages/cli/src/commands/runner/dev/index.ts
Normal file
66
packages/cli/src/commands/runner/dev/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ProjectTypes } from "@definitions/projectTypes";
|
||||
import { getDevtoolsEnvKeyByProjectType, getProjectType } from "@utils/project";
|
||||
import { type 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 "src/utils/package";
|
||||
import { ENV } from "src/utils/env";
|
||||
|
||||
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 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 devtoolsPortEnvKey = getDevtoolsEnvKeyByProjectType(projectType);
|
||||
|
||||
const devtoolsDefault = await isDevtoolsInstalled();
|
||||
|
||||
const devtools = params.devtools === "false" ? false : devtoolsDefault;
|
||||
|
||||
if (devtools) {
|
||||
devtoolsRunner({ exitOnError: false });
|
||||
}
|
||||
|
||||
const envWithDevtoolsPort =
|
||||
devtools && ENV.REFINE_DEVTOOLS_PORT
|
||||
? { [devtoolsPortEnvKey]: ENV.REFINE_DEVTOOLS_PORT }
|
||||
: undefined;
|
||||
|
||||
runScript(binPath, command, envWithDevtoolsPort);
|
||||
};
|
||||
|
||||
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 };
|
||||
463
packages/cli/src/commands/runner/projectScripts.test.ts
Normal file
463
packages/cli/src/commands/runner/projectScripts.test.ts
Normal file
@@ -0,0 +1,463 @@
|
||||
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 "dev" if args is empty', () => {
|
||||
expect(projectScripts[projectType].getDev([])).toEqual(["dev"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStart with empty args", () => {
|
||||
test('should return array with only "preview" 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 "dev" to the args array', () => {
|
||||
const args = ["--arg1", "--arg2"];
|
||||
expect(projectScripts[projectType].getDev(args)).toEqual([
|
||||
"dev",
|
||||
...args,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStart", () => {
|
||||
test('should prepend "preview" 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("NEXTJS project type", () => {
|
||||
const projectType = ProjectTypes.NEXTJS;
|
||||
|
||||
describe("getDev with empty args", () => {
|
||||
test('should return array with only "dev" 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 "dev" 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 "dev" 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 "dev" 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("REMIX_VITE project type", () => {
|
||||
const projectType = ProjectTypes.REMIX_VITE;
|
||||
|
||||
describe("getDev with empty args", () => {
|
||||
test('should return array with only "vite:dev" if args is empty', () => {
|
||||
expect(projectScripts[projectType].getDev([])).toEqual(["vite:dev"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStart with empty args", () => {
|
||||
test("should return default", () => {
|
||||
const logSpy = jest.spyOn(console, "warn");
|
||||
expect(projectScripts[projectType].getStart([])).toEqual([
|
||||
"./build/server/index.js",
|
||||
]);
|
||||
expect(logSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBuild with empty args", () => {
|
||||
test('should return array with only "vite:build" if args is empty', () => {
|
||||
expect(projectScripts[projectType].getBuild([])).toEqual(["vite:build"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDev", () => {
|
||||
test('should prepend "vite:dev" to the args array', () => {
|
||||
const args = ["--arg1", "--arg2"];
|
||||
expect(projectScripts[projectType].getDev(args)).toEqual([
|
||||
"vite:dev",
|
||||
...args,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStart", () => {
|
||||
test("should return args", () => {
|
||||
const args = ["--arg1", "--arg2"];
|
||||
expect(projectScripts[projectType].getStart(args)).toEqual([...args]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBuild", () => {
|
||||
test('should prepend "vite:build" to the args array', () => {
|
||||
const args = ["--arg1", "--arg2"];
|
||||
expect(projectScripts[projectType].getBuild(args)).toEqual([
|
||||
"vite:build",
|
||||
...args,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("REMIX_SPA project type", () => {
|
||||
const projectType = ProjectTypes.REMIX_SPA;
|
||||
|
||||
describe("getDev with empty args", () => {
|
||||
test('should return array with only "vite:dev" if args is empty', () => {
|
||||
expect(projectScripts[projectType].getDev([])).toEqual(["vite:dev"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStart with empty args", () => {
|
||||
test('should return array with only "preview" if args is empty', () => {
|
||||
expect(projectScripts[projectType].getStart([])).toEqual(["preview"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBuild with empty args", () => {
|
||||
test('should return array with only "vite:build" if args is empty', () => {
|
||||
expect(projectScripts[projectType].getBuild([])).toEqual(["vite:build"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDev", () => {
|
||||
test('should prepend "vite:dev" to the args array', () => {
|
||||
const args = ["--arg1", "--arg2"];
|
||||
expect(projectScripts[projectType].getDev(args)).toEqual([
|
||||
"vite:dev",
|
||||
...args,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStart", () => {
|
||||
test('should prepend "preview" to the args array', () => {
|
||||
const args = ["--arg1", "--arg2"];
|
||||
expect(projectScripts[projectType].getStart(args)).toEqual([
|
||||
"preview",
|
||||
...args,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBuild", () => {
|
||||
test('should prepend "vite:build" to the args array', () => {
|
||||
const args = ["--arg1", "--arg2"];
|
||||
expect(projectScripts[projectType].getBuild(args)).toEqual([
|
||||
"vite: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 empty array if args is empty", () => {
|
||||
expect(projectScripts[projectType].getDev([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStart with empty args", () => {
|
||||
test("should return empty array if args is empty", () => {
|
||||
expect(projectScripts[projectType].getStart([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBuild with empty args", () => {
|
||||
test("should return empty array if args is empty", () => {
|
||||
expect(projectScripts[projectType].getBuild([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDev", () => {
|
||||
test("should return the args array as is", () => {
|
||||
const args = ["--arg1", "--arg2"];
|
||||
expect(projectScripts[projectType].getDev(args)).toEqual([...args]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStart", () => {
|
||||
test("should return the args array as is", () => {
|
||||
const args = ["--arg1", "--arg2"];
|
||||
expect(projectScripts[projectType].getStart(args)).toEqual([...args]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBuild", () => {
|
||||
test("should return the args array as is", () => {
|
||||
const args = ["--arg1", "--arg2"];
|
||||
expect(projectScripts[projectType].getBuild(args)).toEqual([...args]);
|
||||
});
|
||||
});
|
||||
});
|
||||
110
packages/cli/src/commands/runner/projectScripts.ts
Normal file
110
packages/cli/src/commands/runner/projectScripts.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
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(`.bin/${binName}`);
|
||||
},
|
||||
},
|
||||
[ProjectTypes.REMIX_VITE]: {
|
||||
getDev: (args: string[]) => ["vite: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/server/index.js`
|
||||
const hasArgs = args?.length;
|
||||
if (hasArgs) {
|
||||
return args;
|
||||
}
|
||||
|
||||
// otherwise print a warning and use `./build/server/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/server/index.js` as default");
|
||||
console.warn("Example: `refine start ./build/server/index.js`");
|
||||
console.log();
|
||||
|
||||
return ["./build/server/index.js"];
|
||||
},
|
||||
getBuild: (args: string[]) => ["vite:build", ...args],
|
||||
getBin: (type?: "dev" | "start" | "build") => {
|
||||
const binName = type === "start" ? "remix-serve" : "remix";
|
||||
return require.resolve(`.bin/${binName}`);
|
||||
},
|
||||
},
|
||||
[ProjectTypes.REMIX_SPA]: {
|
||||
getDev: (args: string[]) => ["vite:dev", ...args],
|
||||
getStart: (args: string[]) => ["preview", ...args],
|
||||
getBuild: (args: string[]) => ["vite:build", ...args],
|
||||
getBin: (type?: "dev" | "start" | "build") => {
|
||||
const binName = type === "start" ? "vite" : "remix";
|
||||
return require.resolve(`.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 type { 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;
|
||||
37
packages/cli/src/commands/runner/runScript.ts
Normal file
37
packages/cli/src/commands/runner/runScript.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ProjectTypes } from "@definitions/projectTypes";
|
||||
import { ENV } from "@utils/env";
|
||||
import execa from "execa";
|
||||
|
||||
export const runScript = async (
|
||||
binPath: string,
|
||||
args: string[],
|
||||
env: Record<string, 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",
|
||||
REFINE_NO_TELEMETRY: ENV.REFINE_NO_TELEMETRY,
|
||||
...env,
|
||||
...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 { type 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;
|
||||
42
packages/cli/src/commands/runner/utils/index.ts
Normal file
42
packages/cli/src/commands/runner/utils/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
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 (runner === "start") {
|
||||
switch (projectType) {
|
||||
case ProjectTypes.REMIX:
|
||||
case ProjectTypes.REMIX_VITE:
|
||||
projectType = "remix-serve" as ProjectTypes;
|
||||
break;
|
||||
case ProjectTypes.REMIX_SPA:
|
||||
projectType = ProjectTypes.VITE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
", ",
|
||||
)}`;
|
||||
};
|
||||
280
packages/cli/src/commands/swizzle/index.tsx
Normal file
280
packages/cli/src/commands/swizzle/index.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import React from "react";
|
||||
import path from "path";
|
||||
import chalk from "chalk";
|
||||
import inquirer from "inquirer";
|
||||
import type { Command, OptionValues } from "commander";
|
||||
import inquirerAutoCompletePrompt from "inquirer-autocomplete-prompt";
|
||||
import { ensureFile, pathExists, readFile, writeFile } from "fs-extra";
|
||||
|
||||
import { getRefineConfig } from "@utils/swizzle";
|
||||
import { prettierFormat } from "@utils/swizzle/prettierFormat";
|
||||
import {
|
||||
getInstalledRefinePackagesFromNodeModules,
|
||||
isPackageHaveRefineConfig,
|
||||
} from "@utils/package";
|
||||
|
||||
import { printSwizzleMessage } from "@components/swizzle-message";
|
||||
|
||||
import type { SwizzleFile } from "@definitions";
|
||||
import { parseSwizzleBlocks } from "@utils/swizzle/parseSwizzleBlocks";
|
||||
import { reorderImports } from "@utils/swizzle/import";
|
||||
import { SWIZZLE_CODES } from "@utils/swizzle/codes";
|
||||
import boxen from "boxen";
|
||||
import { getPathPrefix } from "@utils/swizzle/getPathPrefix";
|
||||
import { installRequiredPackages } from "./install-required-packages";
|
||||
|
||||
const swizzle = (program: Command) => {
|
||||
return program
|
||||
.command("swizzle")
|
||||
.description(
|
||||
`Export a component or a function from ${chalk.bold(
|
||||
"Refine",
|
||||
)} packages to customize it in your project`,
|
||||
)
|
||||
.action(action);
|
||||
};
|
||||
|
||||
const getAutocompleteSource =
|
||||
(
|
||||
rawList: Array<{
|
||||
label: string;
|
||||
group?: string;
|
||||
value?: Record<string, unknown>;
|
||||
}>,
|
||||
) =>
|
||||
(_answers: {}, input = "") => {
|
||||
const filtered = rawList.filter(
|
||||
(el) =>
|
||||
el.label.toLowerCase().includes(input.toLowerCase()) ||
|
||||
el.group?.toLowerCase().includes(input.toLowerCase()),
|
||||
);
|
||||
|
||||
return filtered.flatMap((component, index, arr) => {
|
||||
const hasTitle =
|
||||
component?.group && arr[index - 1]?.group !== component.group;
|
||||
const withTitle =
|
||||
hasTitle && component.group
|
||||
? [new inquirer.Separator(chalk.bold(component.group))]
|
||||
: [];
|
||||
|
||||
return [
|
||||
...withTitle,
|
||||
{
|
||||
name: ` ${component.label}`,
|
||||
value: component?.value ? component.value : component,
|
||||
},
|
||||
];
|
||||
});
|
||||
};
|
||||
|
||||
const action = async (_options: OptionValues) => {
|
||||
inquirer.registerPrompt("autocomplete", inquirerAutoCompletePrompt);
|
||||
|
||||
const installedPackages = await getInstalledRefinePackagesFromNodeModules();
|
||||
|
||||
const packagesWithConfig: Array<{ name: string; path: string }> = [];
|
||||
|
||||
await Promise.all(
|
||||
installedPackages.map(async (pkg) => {
|
||||
const hasConfig = await isPackageHaveRefineConfig(pkg.path);
|
||||
const isNotDuplicate =
|
||||
packagesWithConfig.findIndex((el) => el.name === pkg.name) === -1;
|
||||
if (hasConfig && isNotDuplicate) {
|
||||
packagesWithConfig.push(pkg);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (packagesWithConfig.length === 0) {
|
||||
console.log("No Refine packages found with swizzle configuration.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${boxen(
|
||||
`Found ${chalk.blueBright(
|
||||
packagesWithConfig.length,
|
||||
)} installed ${chalk.blueBright.bold(
|
||||
"Refine",
|
||||
)} packages with swizzle configuration.`,
|
||||
{
|
||||
padding: 1,
|
||||
textAlignment: "center",
|
||||
dimBorder: true,
|
||||
borderColor: "blueBright",
|
||||
borderStyle: "round",
|
||||
},
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
const packageConfigs = await Promise.all(
|
||||
packagesWithConfig.map(async (pkg) => {
|
||||
const config = (await getRefineConfig(pkg.path, true)) ??
|
||||
(await getRefineConfig(pkg.path, false)) ?? {
|
||||
swizzle: { items: [] },
|
||||
};
|
||||
return {
|
||||
...pkg,
|
||||
config,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectedPackage } = await inquirer.prompt<{
|
||||
selectedPackage: (typeof packageConfigs)[number];
|
||||
}>([
|
||||
{
|
||||
type: "autocomplete",
|
||||
pageSize: 10,
|
||||
name: "selectedPackage",
|
||||
message: "Which package do you want to swizzle?",
|
||||
emptyText: "No packages found.",
|
||||
source: getAutocompleteSource(
|
||||
packageConfigs
|
||||
.sort((a, b) =>
|
||||
(a.config?.group ?? "").localeCompare(b.config?.group ?? ""),
|
||||
)
|
||||
.map((pkg) => ({
|
||||
label: pkg.config?.name ?? pkg.name,
|
||||
value: pkg,
|
||||
group: pkg.config?.group,
|
||||
})),
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
const {
|
||||
swizzle: { items, transform },
|
||||
} = selectedPackage.config;
|
||||
|
||||
let selectedComponent: SwizzleFile | undefined = undefined;
|
||||
|
||||
if (items.length === 0) {
|
||||
console.log(
|
||||
`No swizzle items found for ${chalk.bold(
|
||||
selectedPackage.config?.name ?? selectedPackage.name,
|
||||
)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (items.length === 1) {
|
||||
selectedComponent = items[0];
|
||||
} else if (items.length > 1) {
|
||||
const response = await inquirer.prompt<{
|
||||
selectedComponent: SwizzleFile;
|
||||
}>([
|
||||
{
|
||||
type: "list",
|
||||
pageSize: 10,
|
||||
name: "selectedComponent",
|
||||
message: "Which component do you want to swizzle?",
|
||||
emptyText: "No components found.",
|
||||
choices: getAutocompleteSource(
|
||||
items.sort((a, b) => a.group.localeCompare(b.group)),
|
||||
)({}, ""),
|
||||
},
|
||||
]);
|
||||
selectedComponent = response.selectedComponent;
|
||||
}
|
||||
|
||||
if (!selectedComponent) {
|
||||
console.log(
|
||||
`No swizzle items selected for ${chalk.bold(
|
||||
selectedPackage.config?.name ?? selectedPackage.name,
|
||||
)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// this will be prepended to `destPath` values
|
||||
const projectPathPrefix = getPathPrefix();
|
||||
|
||||
const createdFiles = await Promise.all(
|
||||
selectedComponent.files.map(async (file) => {
|
||||
try {
|
||||
const srcPath = file.src
|
||||
? path.join(selectedPackage.path, file.src)
|
||||
: undefined;
|
||||
const destPath = file.dest
|
||||
? path.join(process.cwd(), projectPathPrefix, file.dest)
|
||||
: undefined;
|
||||
|
||||
if (!srcPath) {
|
||||
console.log("No src path found for file", file);
|
||||
return ["", SWIZZLE_CODES.SOURCE_PATH_NOT_FOUND] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
}
|
||||
|
||||
if (!destPath) {
|
||||
console.log("No destination path found for file", file);
|
||||
return ["", SWIZZLE_CODES.TARGET_PATH_NOT_FOUND] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
}
|
||||
|
||||
const hasSrc = await pathExists(srcPath);
|
||||
|
||||
if (!hasSrc) {
|
||||
return [destPath, SWIZZLE_CODES.SOURCE_PATH_NOT_A_FILE] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
}
|
||||
|
||||
const srcContent = await readFile(srcPath, "utf-8");
|
||||
const isDestExist = await pathExists(destPath);
|
||||
|
||||
if (isDestExist) {
|
||||
return [destPath, SWIZZLE_CODES.TARGET_ALREADY_EXISTS] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
}
|
||||
|
||||
await ensureFile(destPath);
|
||||
|
||||
const parsedContent = parseSwizzleBlocks(srcContent);
|
||||
|
||||
const fileTransformedContent =
|
||||
file.transform?.(parsedContent) ?? parsedContent;
|
||||
|
||||
const transformedContent =
|
||||
transform?.(fileTransformedContent, srcPath, destPath) ??
|
||||
fileTransformedContent;
|
||||
|
||||
const reorderedContent = reorderImports(transformedContent);
|
||||
|
||||
const formatted = await prettierFormat(reorderedContent);
|
||||
|
||||
await writeFile(destPath, formatted);
|
||||
|
||||
return [destPath, SWIZZLE_CODES.SUCCESS] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
} catch (error) {
|
||||
return ["", SWIZZLE_CODES.UNKNOWN_ERROR] as [
|
||||
targetPath: string,
|
||||
statusCode: string,
|
||||
];
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (createdFiles.length > 0) {
|
||||
printSwizzleMessage({
|
||||
files: createdFiles,
|
||||
label: selectedComponent.label,
|
||||
message: selectedComponent.message,
|
||||
});
|
||||
|
||||
if (selectedComponent?.requiredPackages?.length) {
|
||||
await installRequiredPackages(selectedComponent.requiredPackages);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default swizzle;
|
||||
@@ -0,0 +1,106 @@
|
||||
import inquirer from "inquirer";
|
||||
import { installPackages, getPreferedPM } from "@utils/package";
|
||||
|
||||
jest.mock("inquirer");
|
||||
jest.mock("@utils/package");
|
||||
|
||||
const getPreferedPMMock = getPreferedPM as jest.MockedFunction<
|
||||
typeof getPreferedPM
|
||||
>;
|
||||
const inquirerMock = inquirer as jest.Mocked<typeof inquirer>;
|
||||
const installPackagesMock = installPackages as jest.MockedFunction<
|
||||
typeof installPackages
|
||||
>;
|
||||
|
||||
import * as installRequiredPackages from "./index";
|
||||
|
||||
describe("should prompt for package installation and install packages if confirmed", () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should install required packages", async () => {
|
||||
inquirerMock.prompt.mockResolvedValueOnce({
|
||||
installRequiredPackages: true,
|
||||
});
|
||||
|
||||
const requiredPackages = ["react", "react-dom"];
|
||||
|
||||
const installRequiredPackagesSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"installRequiredPackages",
|
||||
);
|
||||
const promptForPackageInstallationSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"promptForPackageInstallation",
|
||||
);
|
||||
const displayManualInstallationCommandSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"displayManualInstallationCommand",
|
||||
);
|
||||
|
||||
await installRequiredPackages.installRequiredPackages(requiredPackages);
|
||||
|
||||
expect(installRequiredPackagesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(installRequiredPackagesSpy).toHaveBeenCalledWith(requiredPackages);
|
||||
|
||||
expect(promptForPackageInstallationSpy).toHaveBeenCalledTimes(1);
|
||||
expect(promptForPackageInstallationSpy).toHaveBeenCalledWith(
|
||||
requiredPackages,
|
||||
);
|
||||
|
||||
expect(displayManualInstallationCommandSpy).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(installPackagesMock).toHaveBeenCalledTimes(1);
|
||||
expect(installPackagesMock).toHaveBeenCalledWith(requiredPackages);
|
||||
});
|
||||
|
||||
it("should display manual installation command if not confirmed", async () => {
|
||||
inquirerMock.prompt.mockResolvedValueOnce({
|
||||
installRequiredPackages: false,
|
||||
});
|
||||
getPreferedPMMock.mockResolvedValueOnce({
|
||||
name: "npm",
|
||||
version: "1",
|
||||
});
|
||||
|
||||
const requiredPackages = ["react", "react-dom"];
|
||||
|
||||
const installRequiredPackagesSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"installRequiredPackages",
|
||||
);
|
||||
const promptForPackageInstallationSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"promptForPackageInstallation",
|
||||
);
|
||||
const displayManualInstallationCommandSpy = jest.spyOn(
|
||||
installRequiredPackages,
|
||||
"displayManualInstallationCommand",
|
||||
);
|
||||
const consoleSpy = jest.spyOn(console, "log").mockImplementation();
|
||||
|
||||
await installRequiredPackages.installRequiredPackages(requiredPackages);
|
||||
|
||||
expect(installRequiredPackagesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(installRequiredPackagesSpy).toHaveBeenCalledWith(requiredPackages);
|
||||
|
||||
expect(promptForPackageInstallationSpy).toHaveBeenCalledTimes(1);
|
||||
expect(promptForPackageInstallationSpy).toHaveBeenCalledWith(
|
||||
requiredPackages,
|
||||
);
|
||||
|
||||
expect(displayManualInstallationCommandSpy).toHaveBeenCalledTimes(1);
|
||||
expect(displayManualInstallationCommandSpy).toHaveBeenCalledWith(
|
||||
requiredPackages,
|
||||
);
|
||||
expect(getPreferedPM).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(installPackagesMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"\nYou can install them manually by running this command:",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import inquirer from "inquirer";
|
||||
import chalk from "chalk";
|
||||
import { getPreferedPM, installPackages, pmCommands } from "@utils/package";
|
||||
|
||||
export const installRequiredPackages = async (requiredPackages: string[]) => {
|
||||
const installRequiredPackages =
|
||||
await promptForPackageInstallation(requiredPackages);
|
||||
|
||||
if (!installRequiredPackages) {
|
||||
await displayManualInstallationCommand(requiredPackages);
|
||||
} else {
|
||||
await installPackages(requiredPackages);
|
||||
}
|
||||
};
|
||||
|
||||
export const promptForPackageInstallation = async (
|
||||
requiredPackages: string[],
|
||||
) => {
|
||||
const message =
|
||||
"This component requires following packages to be installed:\n"
|
||||
.concat(requiredPackages.map((pkg) => ` - ${pkg}`).join("\n"))
|
||||
.concat("\nDo you want to install them?");
|
||||
|
||||
const { installRequiredPackages } = await inquirer.prompt<{
|
||||
installRequiredPackages: boolean;
|
||||
}>([
|
||||
{
|
||||
type: "confirm",
|
||||
name: "installRequiredPackages",
|
||||
default: true,
|
||||
message,
|
||||
},
|
||||
]);
|
||||
|
||||
return installRequiredPackages;
|
||||
};
|
||||
|
||||
export const displayManualInstallationCommand = async (
|
||||
requiredPackages: string[],
|
||||
) => {
|
||||
const pm = await getPreferedPM();
|
||||
const pmCommand = pmCommands[pm.name].add.join(" ");
|
||||
const packages = requiredPackages.join(" ");
|
||||
const command = `${pm.name} ${pmCommand} ${packages}`;
|
||||
|
||||
console.log("\nYou can install them manually by running this command:");
|
||||
console.log(chalk.bold.blueBright(command));
|
||||
};
|
||||
232
packages/cli/src/commands/update/index.ts
Normal file
232
packages/cli/src/commands/update/index.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import inquirer from "inquirer";
|
||||
import center from "center-align";
|
||||
import { type Command, Option } from "commander";
|
||||
import PackageJson from "@npmcli/package-json";
|
||||
import spinner from "@utils/spinner";
|
||||
import {
|
||||
getLatestMinorVersionOfPackage,
|
||||
getWantedWithPreferredWildcard,
|
||||
isRefineUptoDate,
|
||||
} from "@commands/check-updates";
|
||||
import { getPreferedPM, installPackages, pmCommands } from "@utils/package";
|
||||
import { promptInteractiveRefineUpdate } from "@commands/update/interactive";
|
||||
import type {
|
||||
PackageDependency,
|
||||
RefinePackageInstalledVersionData,
|
||||
} from "@definitions/package";
|
||||
import { getVersionTable } from "@components/version-table";
|
||||
import chalk from "chalk";
|
||||
|
||||
type SelectedPackage = PackageDependency;
|
||||
|
||||
enum Tag {
|
||||
WANTED = "wanted",
|
||||
LATEST = "latest",
|
||||
NEXT = "next",
|
||||
}
|
||||
|
||||
enum InstallationType {
|
||||
WANTED = "wanted",
|
||||
MINOR = "minor",
|
||||
INTERACTIVE = "interactive",
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let selectedPackages: SelectedPackage | null | undefined = null;
|
||||
|
||||
if (all) {
|
||||
const selectedPackages = await preparePackagesToInstall(tag, packages);
|
||||
if (!selectedPackages) return;
|
||||
|
||||
if (dryRun) {
|
||||
printInstallCommand(selectedPackages);
|
||||
return;
|
||||
}
|
||||
|
||||
runInstallation(selectedPackages);
|
||||
return;
|
||||
}
|
||||
|
||||
// print the table of available updates
|
||||
const { table, width } = getVersionTable(packages) ?? "";
|
||||
console.log(center("Available Updates", width));
|
||||
console.log(table);
|
||||
console.log(
|
||||
`- ${chalk.yellow(
|
||||
chalk.bold("Current"),
|
||||
)}: The version of the package that is currently installed`,
|
||||
);
|
||||
console.log(
|
||||
`- ${chalk.yellow(
|
||||
chalk.bold("Wanted"),
|
||||
)}: The maximum version of the package that satisfies the semver range specified in \`package.json\``,
|
||||
);
|
||||
console.log(
|
||||
`- ${chalk.yellow(
|
||||
chalk.bold("Latest"),
|
||||
)}: The latest version of the package available on npm`,
|
||||
);
|
||||
console.log();
|
||||
|
||||
const { installationType } = await inquirer.prompt<{
|
||||
installationType: InstallationType;
|
||||
}>([
|
||||
{
|
||||
type: "list",
|
||||
name: "installationType",
|
||||
message:
|
||||
"Do you want to update all Refine packages for minor and patch versions?",
|
||||
choices: [
|
||||
{
|
||||
name: `Update all packages to latest "minor" version without any breaking changes.`,
|
||||
value: "minor",
|
||||
},
|
||||
{
|
||||
name: "Update all packages to the latest version that satisfies the semver(`wanted`) range specified in `package.json`",
|
||||
value: "wanted",
|
||||
},
|
||||
{
|
||||
name: `Use interactive mode. Choose this option for "major" version updates.`,
|
||||
value: "interactive",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
if (installationType === InstallationType.INTERACTIVE) {
|
||||
selectedPackages = await promptInteractiveRefineUpdate(packages);
|
||||
}
|
||||
if (installationType === InstallationType.WANTED) {
|
||||
selectedPackages = await preparePackagesToInstall(Tag.WANTED, packages);
|
||||
}
|
||||
if (installationType === InstallationType.MINOR) {
|
||||
selectedPackages = await preparePackagesToInstall(
|
||||
InstallationType.MINOR,
|
||||
packages,
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedPackages) return;
|
||||
|
||||
if (dryRun) {
|
||||
printInstallCommand(selectedPackages);
|
||||
} else {
|
||||
runInstallation(selectedPackages);
|
||||
}
|
||||
};
|
||||
|
||||
const preparePackagesToInstall = async (
|
||||
tag: Tag | InstallationType,
|
||||
packages: RefinePackageInstalledVersionData[],
|
||||
): Promise<PackageDependency | null> => {
|
||||
if (tag === Tag.WANTED) {
|
||||
const isAllPackagesAtWantedVersion = packages.every(
|
||||
(pkg) => pkg.current === pkg.wanted,
|
||||
);
|
||||
if (isAllPackagesAtWantedVersion) {
|
||||
console.log();
|
||||
console.log("✅ All `Refine` packages are already at the wanted version");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// empty line for better readability
|
||||
console.log();
|
||||
|
||||
const packagesWithVersion: PackageDependency = {};
|
||||
for (const pkg of packages) {
|
||||
let version = pkg.latest;
|
||||
|
||||
if (tag === InstallationType.MINOR) {
|
||||
const latestMinorVersion = await spinner(
|
||||
() => getLatestMinorVersionOfPackage(pkg.name, pkg.wanted),
|
||||
`Checking for the latest minor version of ${pkg.name}`,
|
||||
);
|
||||
version = `^${latestMinorVersion}`;
|
||||
}
|
||||
if (tag === Tag.WANTED) {
|
||||
version = getWantedWithPreferredWildcard(pkg.name, pkg.wanted);
|
||||
}
|
||||
if (tag === Tag.LATEST) {
|
||||
version = `^${pkg.latest}`;
|
||||
}
|
||||
if (tag === Tag.NEXT) {
|
||||
version = Tag.NEXT;
|
||||
}
|
||||
|
||||
packagesWithVersion[pkg.name] = version;
|
||||
}
|
||||
|
||||
return packagesWithVersion;
|
||||
};
|
||||
|
||||
const printInstallCommand = async (packages: SelectedPackage) => {
|
||||
const pm = await getPreferedPM();
|
||||
const commandInstall = pmCommands[pm.name].add;
|
||||
let packagesListAsString = "";
|
||||
for (const [name, version] of Object.entries(packages)) {
|
||||
packagesListAsString += `${name}@${version} `;
|
||||
}
|
||||
console.log();
|
||||
console.log(`${pm.name} ${commandInstall.join(" ")} ${packagesListAsString}`);
|
||||
};
|
||||
|
||||
const runInstallation = async (packagesToInstall: SelectedPackage) => {
|
||||
console.log("Updating `Refine` packages...");
|
||||
|
||||
// to install packages, first we need to manipulate the package.json file, then install the packages
|
||||
const packageJson = await PackageJson.load(process.cwd());
|
||||
|
||||
packageJson.update({
|
||||
dependencies: {
|
||||
...((packageJson.content.dependencies ?? {}) as { [x: string]: string }),
|
||||
...(packagesToInstall ?? {}),
|
||||
},
|
||||
});
|
||||
await packageJson.save();
|
||||
|
||||
installPackages([], "all");
|
||||
};
|
||||
|
||||
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: [] as any,
|
||||
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);
|
||||
});
|
||||
});
|
||||
213
packages/cli/src/commands/update/interactive/index.ts
Normal file
213
packages/cli/src/commands/update/interactive/index.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import inquirer from "inquirer";
|
||||
import semverDiff from "semver-diff";
|
||||
import chalk from "chalk";
|
||||
import { findDuplicates } from "@utils/array";
|
||||
import { parsePackageNameAndVersion } from "@utils/package";
|
||||
import type {
|
||||
PackageDependency,
|
||||
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,
|
||||
},
|
||||
]);
|
||||
|
||||
if (answers.packages.length > 0) {
|
||||
// convert to object for easy access
|
||||
const packagesObject: PackageDependency = {};
|
||||
answers.packages.forEach((pckg) => {
|
||||
const { name, version } = parsePackageNameAndVersion(pckg);
|
||||
packagesObject[name] = version ?? "latest";
|
||||
});
|
||||
|
||||
return packagesObject;
|
||||
}
|
||||
|
||||
return 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 type { 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;
|
||||
123
packages/cli/src/components/swizzle-message/index.tsx
Normal file
123
packages/cli/src/components/swizzle-message/index.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
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,48 @@
|
||||
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: "pnpm run refine update",
|
||||
dependencies: ["@refinedev/cli"],
|
||||
scripts: {
|
||||
refine: "refine",
|
||||
},
|
||||
},
|
||||
// has script, no dependency
|
||||
{
|
||||
output: "pnpm run refine update",
|
||||
dependencies: ["@pankod/refine-cli"],
|
||||
scripts: {
|
||||
refine: "refine",
|
||||
},
|
||||
},
|
||||
// no script, has dependency
|
||||
{
|
||||
output: "npx refine update",
|
||||
dependencies: ["@refinedev/cli"],
|
||||
scripts: {},
|
||||
},
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
79
packages/cli/src/components/update-warning-table/table.ts
Normal file
79
packages/cli/src/components/update-warning-table/table.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { RefinePackageInstalledVersionData } from "@definitions/package";
|
||||
import chalk from "chalk";
|
||||
import center from "center-align";
|
||||
import { getDependencies, getPreferedPM, getScripts } from "@utils/package";
|
||||
import { getVersionTable } from "@components/version-table";
|
||||
|
||||
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, width } = getVersionTable(data);
|
||||
console.log();
|
||||
console.log(center("Update Available", width));
|
||||
console.log();
|
||||
console.log(
|
||||
`- ${chalk.yellow(
|
||||
chalk.bold("Current"),
|
||||
)}: The version of the package that is currently installed`,
|
||||
);
|
||||
console.log(
|
||||
`- ${chalk.yellow(
|
||||
chalk.bold("Wanted"),
|
||||
)}: The maximum version of the package that satisfies the semver range specified in \`package.json\``,
|
||||
);
|
||||
console.log(
|
||||
`- ${chalk.yellow(
|
||||
chalk.bold("Latest"),
|
||||
)}: The latest version of the package available on npm`,
|
||||
);
|
||||
console.log(table);
|
||||
console.log(
|
||||
center(
|
||||
`To update ${chalk.bold("`Refine`")} packages with wanted version`,
|
||||
width,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
center(
|
||||
` Run the following command: ${chalk.yellowBright(
|
||||
await getInstallCommand(),
|
||||
)}`,
|
||||
width,
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
151
packages/cli/src/components/version-table/index.ts
Normal file
151
packages/cli/src/components/version-table/index.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import type { RefinePackageInstalledVersionData } from "@definitions/package";
|
||||
import Table from "cli-table3";
|
||||
import chalk from "chalk";
|
||||
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 const getVersionTable = (
|
||||
packages: RefinePackageInstalledVersionData[] = [],
|
||||
) => {
|
||||
const tableHead = Object.keys(packages?.[0] || {});
|
||||
if (!packages || !tableHead.length) return { table: "", width: 0 };
|
||||
|
||||
const terminalWidth = process.stdout.columns || 80;
|
||||
|
||||
const nameColumnWidth =
|
||||
Math.max(...packages.map((row) => row.name.length)) + 2;
|
||||
const versionColumnWidth = 7 + 2;
|
||||
const bordersWidth = 6;
|
||||
const changelogColumnWidth = Math.min(
|
||||
35,
|
||||
terminalWidth - nameColumnWidth - versionColumnWidth * 3 - bordersWidth,
|
||||
);
|
||||
|
||||
const columnWidths = {
|
||||
name: nameColumnWidth,
|
||||
current: versionColumnWidth,
|
||||
wanted: versionColumnWidth,
|
||||
latest: versionColumnWidth,
|
||||
changelog: changelogColumnWidth,
|
||||
} as const;
|
||||
|
||||
const table = new Table({
|
||||
head: orderedColumns,
|
||||
wordWrap: false,
|
||||
wrapOnWordBoundary: true,
|
||||
colWidths: orderedColumns.map((column) => columnWidths[column]),
|
||||
style: {
|
||||
head: ["blue"],
|
||||
},
|
||||
});
|
||||
|
||||
const ellipsisFromCenter = (text: string, length: number) => {
|
||||
// if text is longer than length, cut it from the center to fit the length (add ellipsis)
|
||||
if (text.length > length) {
|
||||
const fitLength = length - 3;
|
||||
const start = text.slice(0, Math.floor(fitLength / 2));
|
||||
const end = text.slice(-Math.ceil(fitLength / 2));
|
||||
return `${start}...${end}`;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
packages.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 tableString = table.toString();
|
||||
const tableWidth = removeANSIColors(
|
||||
tableString.split("\n")?.[0] || "",
|
||||
).length;
|
||||
|
||||
return { table: tableString, width: tableWidth };
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
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";
|
||||
65
packages/cli/src/definitions/package.ts
Normal file
65
packages/cli/src/definitions/package.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export enum PackageManagerTypes {
|
||||
NPM = "npm",
|
||||
YARN = "yarn",
|
||||
PNPM = "pnpm",
|
||||
}
|
||||
|
||||
/**
|
||||
* type of `npm outdated` command response
|
||||
*/
|
||||
export type NpmOutdatedResponse = Record<
|
||||
string,
|
||||
{
|
||||
current: string;
|
||||
wanted: string;
|
||||
latest: string;
|
||||
dependent: string;
|
||||
location: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export type RefinePackageInstalledVersionData = {
|
||||
name: string;
|
||||
/**
|
||||
* version of the package that is currently installed. Without semver range wildcard.
|
||||
*/
|
||||
current: string;
|
||||
/**
|
||||
* version that the user wants to update to. Without semver range wildcard.
|
||||
* e.g. `^1.0.0` in `package.json` resolves to `1.0.1` in the this field.
|
||||
*/
|
||||
wanted: string;
|
||||
/**
|
||||
* latest version of the package available on npm
|
||||
*/
|
||||
latest: string;
|
||||
/**
|
||||
* changelog url
|
||||
*/
|
||||
changelog?: string;
|
||||
/**
|
||||
* dependent package name
|
||||
*/
|
||||
dependent: string;
|
||||
/**
|
||||
* location of the package
|
||||
*/
|
||||
location: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* key is the script name and value is the script command
|
||||
*/
|
||||
export type PackageDependency = Record<string, string>;
|
||||
|
||||
export type PackageJson = {
|
||||
name: string;
|
||||
version: string;
|
||||
scripts?: Record<string, string>;
|
||||
dependencies?: PackageDependency;
|
||||
devDependencies?: PackageDependency;
|
||||
peerDependencies?: PackageDependency;
|
||||
refine?: {
|
||||
projectId?: string;
|
||||
};
|
||||
};
|
||||
11
packages/cli/src/definitions/projectTypes.ts
Normal file
11
packages/cli/src/definitions/projectTypes.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export enum ProjectTypes {
|
||||
REACT_SCRIPT = "react-scripts",
|
||||
REMIX = "remix",
|
||||
REMIX_VITE = "remix-vite",
|
||||
REMIX_SPA = "remix-spa",
|
||||
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/index.js";
|
||||
export { getImports, getNameChangeInImport } from "./utils/swizzle/import.js";
|
||||
export { appendAfterImports } from "./utils/swizzle/appendAfterImports.js";
|
||||
export { getFileContent } from "./utils/swizzle/getFileContent.js";
|
||||
export type { ImportMatch, NameChangeMatch } from "./utils/swizzle/import.js";
|
||||
51
packages/cli/src/telemetry/index.ts
Normal file
51
packages/cli/src/telemetry/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { NODE_ENV } from "@definitions/node";
|
||||
import type { 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) {}
|
||||
};
|
||||
112
packages/cli/src/transformers/add-devtools-component.ts
Normal file
112
packages/cli/src/transformers/add-devtools-component.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type {
|
||||
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",
|
||||
"--ignore-pattern=dist",
|
||||
]);
|
||||
|
||||
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 };
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ant-design transformer should transform the file
|
||||
----------------------------------------------------
|
||||
import {
|
||||
GitHubBanner,
|
||||
Refine,
|
||||
ErrorComponent,
|
||||
WelcomePage,
|
||||
} from "@refinedev/core";
|
||||
import dataProvider from "@refinedev/simple-rest";
|
||||
import routerProvider, {
|
||||
UnsavedChangesNotifier,
|
||||
DocumentTitleHandler,
|
||||
} from "@refinedev/react-router-v6";
|
||||
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<GitHubBanner />
|
||||
<Refine
|
||||
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
|
||||
routerProvider={routerProvider}
|
||||
options={{
|
||||
syncWithLocation: true,
|
||||
warnWhenUnsavedChanges: true,
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route index element={<WelcomePage />} />
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Routes>
|
||||
<UnsavedChangesNotifier />
|
||||
<DocumentTitleHandler />
|
||||
</Refine>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
---------------------------------------------------- 1`] = `
|
||||
"import { GitHubBanner, Refine, WelcomePage } from "@refinedev/core";
|
||||
import {
|
||||
useNotificationProvider,
|
||||
RefineThemes,
|
||||
ThemedLayoutV2,
|
||||
ErrorComponent,
|
||||
} from "@refinedev/antd";
|
||||
import dataProvider from "@refinedev/simple-rest";
|
||||
import routerProvider, {
|
||||
UnsavedChangesNotifier,
|
||||
DocumentTitleHandler,
|
||||
} from "@refinedev/react-router-v6";
|
||||
|
||||
import { BrowserRouter, Route, Routes, Outlet } from "react-router-dom";
|
||||
|
||||
import { ConfigProvider, App as AntdApp } from "antd";
|
||||
import "@refinedev/antd/dist/reset.css";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<GitHubBanner />
|
||||
<ConfigProvider theme={RefineThemes.Blue}>
|
||||
<AntdApp>
|
||||
<Refine
|
||||
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
|
||||
routerProvider={routerProvider}
|
||||
options={{
|
||||
syncWithLocation: true,
|
||||
warnWhenUnsavedChanges: true,
|
||||
}}
|
||||
notificationProvider={useNotificationProvider}
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<ThemedLayoutV2>
|
||||
<Outlet />
|
||||
</ThemedLayoutV2>
|
||||
}
|
||||
>
|
||||
<Route index element={<WelcomePage />} />
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<UnsavedChangesNotifier />
|
||||
<DocumentTitleHandler />
|
||||
</Refine>
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;"
|
||||
`;
|
||||
|
||||
exports[`ant-design transformer should transform the file
|
||||
----------------------------------------------------
|
||||
import { Refine, WelcomePage } from "@refinedev/core";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Refine>
|
||||
<WelcomePage />
|
||||
</Refine>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
---------------------------------------------------- 1`] = `
|
||||
"import { Refine, WelcomePage } from "@refinedev/core";
|
||||
|
||||
import { useNotificationProvider, RefineThemes } from "@refinedev/antd";
|
||||
import { ConfigProvider, App as AntdApp } from "antd";
|
||||
import "@refinedev/antd/dist/reset.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider theme={RefineThemes.Blue}>
|
||||
<AntdApp>
|
||||
<Refine notificationProvider={useNotificationProvider}>
|
||||
<WelcomePage />
|
||||
</Refine>
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;"
|
||||
`;
|
||||
|
||||
exports[`ant-design transformer should transform the file
|
||||
----------------------------------------------------
|
||||
import { Refine, WelcomePage } from "@refinedev/core";
|
||||
|
||||
import { useNotificationProvider, RefineThemes } from "@refinedev/antd";
|
||||
import { ConfigProvider, App as AntdApp } from "antd";
|
||||
import "@refinedev/antd/dist/reset.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider theme={RefineThemes.Blue}>
|
||||
<AntdApp>
|
||||
<Refine notificationProvider={useNotificationProvider}>
|
||||
<WelcomePage />
|
||||
</Refine>
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
---------------------------------------------------- 1`] = `
|
||||
"import { Refine, WelcomePage } from "@refinedev/core";
|
||||
|
||||
import { useNotificationProvider, RefineThemes } from "@refinedev/antd";
|
||||
import { ConfigProvider, App as AntdApp } from "antd";
|
||||
import "@refinedev/antd/dist/reset.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider theme={RefineThemes.Blue}>
|
||||
<AntdApp>
|
||||
<Refine notificationProvider={useNotificationProvider}>
|
||||
<WelcomePage />
|
||||
</Refine>
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;"
|
||||
`;
|
||||
@@ -0,0 +1,124 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`react-router transformer should transform the file
|
||||
----------------------------------------------------
|
||||
import { Refine, WelcomePage } from "@refinedev/core";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Refine>
|
||||
<WelcomePage />
|
||||
</Refine>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
---------------------------------------------------- 1`] = `
|
||||
"import { Refine, WelcomePage, ErrorComponent } from "@refinedev/core";
|
||||
|
||||
import routerProvider from "@refinedev/react-router-v6";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Refine routerProvider={routerProvider}>
|
||||
<Routes>
|
||||
<Route index element={<WelcomePage />} />
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Routes>
|
||||
</Refine>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;"
|
||||
`;
|
||||
|
||||
exports[`react-router transformer should transform the file
|
||||
----------------------------------------------------
|
||||
import { Refine, WelcomePage } from "@refinedev/core";
|
||||
|
||||
import { UseListExample, UseOneExample, UseUpdateExample } from 'examples';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Refine>
|
||||
<WelcomePage />
|
||||
<UseListExample />
|
||||
<UseOneExample />
|
||||
<UseUpdateExample />
|
||||
</Refine>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
---------------------------------------------------- 1`] = `
|
||||
"import { Refine, WelcomePage, ErrorComponent } from "@refinedev/core";
|
||||
|
||||
import routerProvider from "@refinedev/react-router-v6";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
|
||||
import { UseListExample, UseOneExample, UseUpdateExample } from "examples";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Refine routerProvider={routerProvider}>
|
||||
<Routes>
|
||||
<Route index element={<WelcomePage />} />
|
||||
<Route path="/use-list-example" element={<UseListExample />} />
|
||||
<Route path="/use-one-example" element={<UseOneExample />} />
|
||||
<Route path="/use-update-example" element={<UseUpdateExample />} />
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Routes>
|
||||
</Refine>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;"
|
||||
`;
|
||||
|
||||
exports[`react-router transformer should transform the file
|
||||
----------------------------------------------------
|
||||
import { Refine, WelcomePage, ErrorComponent } from "@refinedev/core";
|
||||
|
||||
import routerProvider from "@refinedev/react-router-v6";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Refine routerProvider={routerProvider}>
|
||||
<Routes>
|
||||
<Route index element={<WelcomePage />} />
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Routes>
|
||||
</Refine>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
---------------------------------------------------- 1`] = `
|
||||
"import { Refine, WelcomePage, ErrorComponent } from "@refinedev/core";
|
||||
|
||||
import routerProvider from "@refinedev/react-router-v6";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Refine routerProvider={routerProvider}>
|
||||
<Routes>
|
||||
<Route index element={<WelcomePage />} />
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Routes>
|
||||
</Refine>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;"
|
||||
`;
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { FileInfo } from "jscodeshift";
|
||||
import transform from "./ant-design";
|
||||
import { VITE_STARTER_SOURCE } from "./fixtures/vite-starter";
|
||||
|
||||
import jscodeshift from "jscodeshift";
|
||||
import { BASE_HEADLESS_SOURCE } from "./fixtures/ant-design/base-headless";
|
||||
import { WITH_EXISTING_ANT_DESIGN_SETUP_SOURCE } from "./fixtures/ant-design/with-existing-ant-design-setup";
|
||||
|
||||
describe("ant-design transformer", () => {
|
||||
const cases = [
|
||||
VITE_STARTER_SOURCE,
|
||||
BASE_HEADLESS_SOURCE,
|
||||
WITH_EXISTING_ANT_DESIGN_SETUP_SOURCE,
|
||||
];
|
||||
|
||||
it.each(cases)(
|
||||
"should transform the file\n----------------------------------------------------\n%s\n----------------------------------------------------",
|
||||
async (source) => {
|
||||
const fileInfo: FileInfo = {
|
||||
path: "App.tsx",
|
||||
source,
|
||||
};
|
||||
|
||||
const result = await transform(fileInfo, {
|
||||
jscodeshift,
|
||||
j: jscodeshift,
|
||||
stats: () => {
|
||||
// do nothing
|
||||
},
|
||||
report: () => {
|
||||
// do nothing
|
||||
},
|
||||
});
|
||||
|
||||
expect(result?.trim()).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
181
packages/cli/src/transformers/integrations/ant-design.ts
Normal file
181
packages/cli/src/transformers/integrations/ant-design.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import {
|
||||
addAttributeIfNotExist,
|
||||
addOrUpdateImports,
|
||||
addOrUpdateNamelessImport,
|
||||
removeImportIfExists,
|
||||
wrapElement,
|
||||
} from "../../utils/codeshift";
|
||||
import { prettierFormat } from "../../utils/swizzle/prettierFormat";
|
||||
import type { API, Collection, FileInfo, JSCodeshift } from "jscodeshift";
|
||||
|
||||
export const parser = "tsx";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
addAntDesignImports(j, source);
|
||||
|
||||
addOutletImport(j, source);
|
||||
|
||||
refineElement.forEach((element) => {
|
||||
addAttributeIfNotExist(
|
||||
j,
|
||||
source,
|
||||
element,
|
||||
"notificationProvider",
|
||||
j.jsxExpressionContainer(j.identifier("useNotificationProvider")),
|
||||
);
|
||||
|
||||
const antdApp = wrapElement(j, source, element, "AntdApp");
|
||||
|
||||
wrapElement(j, source, antdApp, "ConfigProvider", [
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("theme"),
|
||||
j.jsxExpressionContainer(j.identifier("RefineThemes.Blue")),
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
const routesElement = source.find(j.JSXElement, {
|
||||
openingElement: {
|
||||
name: {
|
||||
name: "Routes",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (routesElement.length > 0) {
|
||||
addOrUpdateImports(j, source, "@refinedev/antd", [
|
||||
"ThemedLayoutV2",
|
||||
"ErrorComponent",
|
||||
]);
|
||||
|
||||
routesElement.forEach((element) => {
|
||||
removeImportIfExists(j, source, "@refinedev/core", "ErrorComponent");
|
||||
|
||||
const layoutChildren = [...(element.node.children ?? [])];
|
||||
|
||||
const isErrorRouteExists = source.find(j.JSXElement, {
|
||||
openingElement: {
|
||||
name: {
|
||||
name: "ErrorComponent",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!isErrorRouteExists) {
|
||||
const errorRoute = j.jsxElement(
|
||||
j.jsxOpeningElement(
|
||||
j.jsxIdentifier("Route"),
|
||||
[
|
||||
j.jsxAttribute(j.jsxIdentifier("path"), j.literal("*")),
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("element"),
|
||||
j.jsxExpressionContainer(
|
||||
j.jsxElement(
|
||||
j.jsxOpeningElement(
|
||||
j.jsxIdentifier("ErrorComponent"),
|
||||
[],
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
true,
|
||||
),
|
||||
);
|
||||
layoutChildren.push(errorRoute);
|
||||
}
|
||||
|
||||
const existingThemedLayout = source.find(j.JSXElement, {
|
||||
openingElement: {
|
||||
name: {
|
||||
name: "ThemedLayoutV2",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingThemedLayout.length) {
|
||||
const antdLayout = j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier("Route"), [
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("element"),
|
||||
j.jsxExpressionContainer(
|
||||
j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier("ThemedLayoutV2"), []),
|
||||
j.jsxClosingElement(j.jsxIdentifier("ThemedLayoutV2")),
|
||||
[
|
||||
j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier("Outlet"), [], true),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
j.jsxClosingElement(j.jsxIdentifier("Route")),
|
||||
layoutChildren,
|
||||
);
|
||||
|
||||
element.replace(
|
||||
j.jsxElement(
|
||||
element.node.openingElement,
|
||||
element.node.closingElement,
|
||||
[antdLayout],
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return await prettierFormat(source.toSource());
|
||||
}
|
||||
|
||||
export const addAntDesignImports = (j: JSCodeshift, source: Collection) => {
|
||||
addOrUpdateImports(
|
||||
j,
|
||||
source,
|
||||
"@refinedev/antd",
|
||||
["useNotificationProvider", "RefineThemes"],
|
||||
(sourceImports, targetImport) => {
|
||||
sourceImports.at(0).insertAfter(targetImport);
|
||||
},
|
||||
);
|
||||
addOrUpdateImports(
|
||||
j,
|
||||
source,
|
||||
"antd",
|
||||
["ConfigProvider", "App as AntdApp"],
|
||||
(sourceImports, targetImport) => {
|
||||
sourceImports.at(-1).insertAfter(targetImport);
|
||||
},
|
||||
);
|
||||
addOrUpdateNamelessImport(
|
||||
j,
|
||||
source,
|
||||
"@refinedev/antd/dist/reset.css",
|
||||
(sourceImports, targetImport) => {
|
||||
sourceImports.at(-1).insertAfter(targetImport);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const addOutletImport = (j: JSCodeshift, source: Collection) => {
|
||||
addOrUpdateImports(j, source, "react-router-dom", ["Outlet"]);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
export const BASE_HEADLESS_SOURCE = `
|
||||
import {
|
||||
GitHubBanner,
|
||||
Refine,
|
||||
ErrorComponent,
|
||||
WelcomePage,
|
||||
} from "@refinedev/core";
|
||||
import dataProvider from "@refinedev/simple-rest";
|
||||
import routerProvider, {
|
||||
UnsavedChangesNotifier,
|
||||
DocumentTitleHandler,
|
||||
} from "@refinedev/react-router-v6";
|
||||
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<GitHubBanner />
|
||||
<Refine
|
||||
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
|
||||
routerProvider={routerProvider}
|
||||
options={{
|
||||
syncWithLocation: true,
|
||||
warnWhenUnsavedChanges: true,
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route index element={<WelcomePage />} />
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Routes>
|
||||
<UnsavedChangesNotifier />
|
||||
<DocumentTitleHandler />
|
||||
</Refine>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
`.trim();
|
||||
@@ -0,0 +1,20 @@
|
||||
export const WITH_EXISTING_ANT_DESIGN_SETUP_SOURCE = `
|
||||
import { Refine, WelcomePage } from "@refinedev/core";
|
||||
|
||||
import { useNotificationProvider, RefineThemes } from "@refinedev/antd";
|
||||
import { ConfigProvider, App as AntdApp } from "antd";
|
||||
import "@refinedev/antd/dist/reset.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider theme={RefineThemes.Blue}>
|
||||
<AntdApp>
|
||||
<Refine notificationProvider={useNotificationProvider}>
|
||||
<WelcomePage />
|
||||
</Refine>
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;`.trim();
|
||||
@@ -0,0 +1,18 @@
|
||||
export const WITH_COMPONENT_IMPORTS_SOURCE = `
|
||||
import { Refine, WelcomePage } from "@refinedev/core";
|
||||
|
||||
import { UseListExample, UseOneExample, UseUpdateExample } from 'examples';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Refine>
|
||||
<WelcomePage />
|
||||
<UseListExample />
|
||||
<UseOneExample />
|
||||
<UseUpdateExample />
|
||||
</Refine>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
`.trim();
|
||||
@@ -0,0 +1,21 @@
|
||||
export const WITH_EXISTING_REACT_ROUTER_SETUP_SOURCE = `
|
||||
import { Refine, WelcomePage, ErrorComponent } from "@refinedev/core";
|
||||
|
||||
import routerProvider from "@refinedev/react-router-v6";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Refine routerProvider={routerProvider}>
|
||||
<Routes>
|
||||
<Route index element={<WelcomePage />} />
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Routes>
|
||||
</Refine>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
`.trim();
|
||||
@@ -0,0 +1,13 @@
|
||||
export const VITE_STARTER_SOURCE = `
|
||||
import { Refine, WelcomePage } from "@refinedev/core";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Refine>
|
||||
<WelcomePage />
|
||||
</Refine>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
`.trim();
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { FileInfo } from "jscodeshift";
|
||||
import transform from "./react-router";
|
||||
import { VITE_STARTER_SOURCE } from "./fixtures/vite-starter";
|
||||
|
||||
import jscodeshift from "jscodeshift";
|
||||
import { WITH_COMPONENT_IMPORTS_SOURCE } from "./fixtures/react-router/with-component-imports";
|
||||
import { WITH_EXISTING_REACT_ROUTER_SETUP_SOURCE } from "./fixtures/react-router/with-existing-react-router-setup";
|
||||
|
||||
describe("react-router transformer", () => {
|
||||
const cases = [
|
||||
VITE_STARTER_SOURCE,
|
||||
WITH_COMPONENT_IMPORTS_SOURCE,
|
||||
WITH_EXISTING_REACT_ROUTER_SETUP_SOURCE,
|
||||
];
|
||||
|
||||
it.each(cases)(
|
||||
"should transform the file\n----------------------------------------------------\n%s\n----------------------------------------------------",
|
||||
async (source) => {
|
||||
const fileInfo: FileInfo = {
|
||||
path: "App.tsx",
|
||||
source,
|
||||
};
|
||||
|
||||
const result = await transform(fileInfo, {
|
||||
jscodeshift,
|
||||
j: jscodeshift,
|
||||
stats: () => {
|
||||
// do nothing
|
||||
},
|
||||
report: () => {
|
||||
// do nothing
|
||||
},
|
||||
});
|
||||
|
||||
expect(result?.trim()).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
192
packages/cli/src/transformers/integrations/react-router.ts
Normal file
192
packages/cli/src/transformers/integrations/react-router.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import decamelize from "decamelize";
|
||||
import type {
|
||||
API,
|
||||
Collection,
|
||||
FileInfo,
|
||||
JSCodeshift,
|
||||
JSXIdentifier,
|
||||
} from "jscodeshift";
|
||||
import {
|
||||
addAttributeIfNotExist,
|
||||
addOrUpdateImports,
|
||||
wrapChildren,
|
||||
wrapElement,
|
||||
} from "../../utils/codeshift";
|
||||
import { prettierFormat } from "../../utils/swizzle/prettierFormat";
|
||||
|
||||
export const parser = "tsx";
|
||||
|
||||
export default async function transformer(file: FileInfo, api: API) {
|
||||
const j = api.jscodeshift;
|
||||
const source = j(file.source);
|
||||
|
||||
const hasRefineElement =
|
||||
source.find(j.JSXElement, {
|
||||
openingElement: {
|
||||
name: {
|
||||
name: "Refine",
|
||||
},
|
||||
},
|
||||
}).length !== 0;
|
||||
|
||||
if (!hasRefineElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const functionDeclaration = source.find(j.FunctionDeclaration, {
|
||||
id: {
|
||||
name: "App",
|
||||
},
|
||||
});
|
||||
|
||||
addReactRouterImports(j, source);
|
||||
|
||||
// Find the first jsx element in the function and wrap it with `BrowserRouter` component.
|
||||
|
||||
functionDeclaration.forEach((funcDec) => {
|
||||
const openerElements = j(funcDec).find(j.JSXElement).at(0);
|
||||
|
||||
openerElements.forEach((openerElement) => {
|
||||
wrapElement(j, source, openerElement, "BrowserRouter");
|
||||
});
|
||||
});
|
||||
|
||||
const refineElements = source.find(j.JSXElement, {
|
||||
openingElement: {
|
||||
name: {
|
||||
name: "Refine",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
refineElements.forEach((refineElement) => {
|
||||
addAttributeIfNotExist(
|
||||
j,
|
||||
source,
|
||||
refineElement,
|
||||
"routerProvider",
|
||||
j.jsxExpressionContainer(j.identifier("routerProvider")),
|
||||
);
|
||||
|
||||
wrapChildren(j, source, refineElement, "Routes");
|
||||
});
|
||||
|
||||
const routesElements = source.find(j.JSXElement, {
|
||||
openingElement: {
|
||||
name: {
|
||||
name: "Routes",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
routesElements.forEach((routesElement) => {
|
||||
let elementCount = 0;
|
||||
|
||||
const newRouteElements = [];
|
||||
|
||||
routesElement.node?.children
|
||||
?.filter((value) => {
|
||||
return (
|
||||
value.type === "JSXElement" &&
|
||||
value.openingElement.name.type === "JSXIdentifier" &&
|
||||
value.openingElement.name.name !== "Route"
|
||||
);
|
||||
})
|
||||
?.forEach((child) => {
|
||||
if (child.type === "JSXElement") {
|
||||
elementCount++;
|
||||
const firstAttribute =
|
||||
elementCount === 1
|
||||
? j.jsxAttribute(j.jsxIdentifier("index"))
|
||||
: j.jsxAttribute(
|
||||
j.jsxIdentifier("path"),
|
||||
j.stringLiteral(
|
||||
`/${decamelize(
|
||||
(child.openingElement.name as JSXIdentifier).name,
|
||||
{ separator: "-" },
|
||||
)}`,
|
||||
),
|
||||
);
|
||||
|
||||
const routeElement = j.jsxElement(
|
||||
j.jsxOpeningElement(
|
||||
j.jsxIdentifier("Route"),
|
||||
[
|
||||
firstAttribute,
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("element"),
|
||||
j.jsxExpressionContainer(j.jsxElement(child.openingElement)),
|
||||
),
|
||||
],
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
||||
newRouteElements.push(routeElement);
|
||||
}
|
||||
});
|
||||
|
||||
if (newRouteElements.length === 0) {
|
||||
newRouteElements.push(...(routesElement.node.children ?? []));
|
||||
} else {
|
||||
addOrUpdateImports(j, source, "@refinedev/core", ["ErrorComponent"]);
|
||||
|
||||
const errorRoute = j.jsxElement(
|
||||
j.jsxOpeningElement(
|
||||
j.jsxIdentifier("Route"),
|
||||
[
|
||||
j.jsxAttribute(j.jsxIdentifier("path"), j.literal("*")),
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("element"),
|
||||
j.jsxExpressionContainer(
|
||||
j.jsxElement(
|
||||
j.jsxOpeningElement(
|
||||
j.jsxIdentifier("ErrorComponent"),
|
||||
[],
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
||||
newRouteElements.push(errorRoute);
|
||||
}
|
||||
|
||||
routesElement.replace(
|
||||
j.jsxElement(
|
||||
routesElement.node.openingElement,
|
||||
routesElement.node.closingElement,
|
||||
newRouteElements,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return await prettierFormat(source.toSource());
|
||||
}
|
||||
|
||||
export const addReactRouterImports = (j: JSCodeshift, source: Collection) => {
|
||||
addOrUpdateImports(
|
||||
j,
|
||||
source,
|
||||
"react-router-dom",
|
||||
["BrowserRouter", "Routes", "Route"],
|
||||
(sourceDeclarations, targetDeclaration) => {
|
||||
sourceDeclarations.at(0).insertAfter(targetDeclaration);
|
||||
},
|
||||
);
|
||||
|
||||
addOrUpdateImports(
|
||||
j,
|
||||
source,
|
||||
"@refinedev/react-router-v6",
|
||||
["routerProvider"],
|
||||
(sourceDeclarations, targetDeclaration) => {
|
||||
sourceDeclarations.at(0).insertAfter(targetDeclaration);
|
||||
},
|
||||
true,
|
||||
);
|
||||
};
|
||||
120
packages/cli/src/transformers/resource.ts
Normal file
120
packages/cli/src/transformers/resource.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type {
|
||||
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();
|
||||
}
|
||||
112
packages/cli/src/update-notifier/index.test.ts
Normal file
112
packages/cli/src/update-notifier/index.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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);
|
||||
});
|
||||
142
packages/cli/src/update-notifier/index.tsx
Normal file
142
packages/cli/src/update-notifier/index.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import Conf from "conf";
|
||||
import chalk from "chalk";
|
||||
import { isRefineUptoDate } from "@commands/check-updates";
|
||||
import { printUpdateWarningTable } from "@components/update-warning-table";
|
||||
import type { 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 type { 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);
|
||||
};
|
||||
193
packages/cli/src/utils/codeshift/index.ts
Normal file
193
packages/cli/src/utils/codeshift/index.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import type {
|
||||
ASTPath,
|
||||
Collection,
|
||||
ImportDeclaration,
|
||||
JSCodeshift,
|
||||
JSXAttribute,
|
||||
JSXElement,
|
||||
JSXExpressionContainer,
|
||||
} from "jscodeshift";
|
||||
|
||||
export const wrapElement = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
element: ASTPath<JSXElement>,
|
||||
wrapper: string,
|
||||
wrapperAttributes: JSXAttribute[] = [],
|
||||
) => {
|
||||
const existingWrapperElement = source.find(j.JSXElement, {
|
||||
openingElement: { name: { name: wrapper } },
|
||||
});
|
||||
|
||||
if (existingWrapperElement.length) {
|
||||
return element;
|
||||
}
|
||||
|
||||
const wrapperElement = j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier(wrapper), wrapperAttributes),
|
||||
j.jsxClosingElement(j.jsxIdentifier(wrapper)),
|
||||
[
|
||||
j.jsxElement(
|
||||
element.node.openingElement,
|
||||
element.node.closingElement,
|
||||
element.node.children,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
j(element).replaceWith(wrapperElement);
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
export const wrapChildren = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
element: ASTPath<JSXElement>,
|
||||
wrapper: string,
|
||||
wrapperAttributes: JSXAttribute[] = [],
|
||||
) => {
|
||||
const existingWrapperElement = source.find(j.JSXElement, {
|
||||
openingElement: { name: { name: wrapper } },
|
||||
});
|
||||
|
||||
if (existingWrapperElement.length) {
|
||||
return element;
|
||||
}
|
||||
|
||||
const wrapperElement = j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier(wrapper), wrapperAttributes),
|
||||
j.jsxClosingElement(j.jsxIdentifier(wrapper)),
|
||||
element.value.children,
|
||||
);
|
||||
|
||||
j(element).replaceWith(
|
||||
j.jsxElement(element.node.openingElement, element.node.closingElement, [
|
||||
wrapperElement,
|
||||
]),
|
||||
);
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
export const addAttributeIfNotExist = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
element: ASTPath<JSXElement>,
|
||||
attributeIdentifier: string,
|
||||
attributeValue?: JSXElement | JSXExpressionContainer,
|
||||
) => {
|
||||
const existingAttribute = source.find(j.JSXAttribute, {
|
||||
name: {
|
||||
name: attributeIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingAttribute.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attribute = j.jsxAttribute(
|
||||
j.jsxIdentifier(attributeIdentifier),
|
||||
attributeValue ? attributeValue : undefined,
|
||||
);
|
||||
|
||||
element.node.openingElement.attributes?.push(attribute);
|
||||
};
|
||||
|
||||
export const addOrUpdateImports = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
importPath: string,
|
||||
importIdentifierNames: string[],
|
||||
insertFunc?: (
|
||||
sourceDeclaration: Collection<ImportDeclaration>,
|
||||
targetDeclaration: ImportDeclaration,
|
||||
) => void,
|
||||
isDefault = false,
|
||||
) => {
|
||||
const existingImports = source.find(j.ImportDeclaration, {
|
||||
source: {
|
||||
value: importPath,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefault && existingImports.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const specifierFunc = isDefault
|
||||
? j.importDefaultSpecifier
|
||||
: j.importSpecifier;
|
||||
|
||||
const importSpecifiers = importIdentifierNames.map((importIdentifierName) =>
|
||||
specifierFunc(j.identifier(importIdentifierName)),
|
||||
);
|
||||
|
||||
if (existingImports.length) {
|
||||
// Check existing imports in the `ImportDeclaration` to avoid duplicate imports
|
||||
const nonExistingImportIdentifiers = importIdentifierNames.filter(
|
||||
(importIdentifierName) =>
|
||||
existingImports.find(j.ImportSpecifier).filter((path) => {
|
||||
return path.node.imported.name === importIdentifierName.split(" ")[0];
|
||||
}).length === 0,
|
||||
);
|
||||
|
||||
if (nonExistingImportIdentifiers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nonExistingSpecifiers = nonExistingImportIdentifiers.map(
|
||||
(importIdentifierName) =>
|
||||
specifierFunc(j.identifier(importIdentifierName)),
|
||||
);
|
||||
|
||||
existingImports
|
||||
.at(0)
|
||||
.get("specifiers")
|
||||
.value.push(...nonExistingSpecifiers);
|
||||
} else {
|
||||
const importDeclaration = j.importDeclaration(
|
||||
importSpecifiers,
|
||||
j.literal(importPath),
|
||||
);
|
||||
|
||||
insertFunc?.(source.find(j.ImportDeclaration), importDeclaration);
|
||||
}
|
||||
};
|
||||
|
||||
export const addOrUpdateNamelessImport = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
importPath: string,
|
||||
insertFunc: (
|
||||
sourceDeclaration: Collection<ImportDeclaration>,
|
||||
targetDeclaration: ImportDeclaration,
|
||||
) => void,
|
||||
) => {
|
||||
const existingImports = source.find(j.ImportDeclaration, {
|
||||
source: {
|
||||
value: importPath,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingImports.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importDeclaration = j.importDeclaration([], j.literal(importPath));
|
||||
|
||||
insertFunc(source.find(j.ImportDeclaration), importDeclaration);
|
||||
};
|
||||
|
||||
export const removeImportIfExists = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
importPath: string,
|
||||
importIdentifierName: string,
|
||||
) => {
|
||||
source
|
||||
.find(j.ImportDeclaration, { source: { value: importPath } })
|
||||
.find(j.ImportSpecifier, { imported: { name: importIdentifierName } })
|
||||
.remove();
|
||||
};
|
||||
76
packages/cli/src/utils/compile/index.ts
Normal file
76
packages/cli/src/utils/compile/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import Handlebars from "handlebars";
|
||||
import {
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
createFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from "fs-extra";
|
||||
import { getComponentNameByResource } from "@utils/resource";
|
||||
|
||||
export const compile = (filePath: string, params: any): string => {
|
||||
const content = readFileSync(filePath);
|
||||
|
||||
Handlebars.registerHelper("ifIn", (elem, list, options) => {
|
||||
if (elem.includes(list)) {
|
||||
return options.fn(Handlebars);
|
||||
}
|
||||
return options.inverse(Handlebars);
|
||||
});
|
||||
|
||||
Handlebars.registerHelper("formatInferencerComponent", (string) => {
|
||||
if (!string) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (string) {
|
||||
case "chakra-ui":
|
||||
return "ChakraUI";
|
||||
|
||||
default:
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
});
|
||||
|
||||
Handlebars.registerHelper("capitalize", (string) => {
|
||||
if (!string) {
|
||||
return;
|
||||
}
|
||||
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
});
|
||||
|
||||
Handlebars.registerHelper("getComponentNameByResource", (string) => {
|
||||
if (!string) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getComponentNameByResource(string);
|
||||
});
|
||||
|
||||
const template = Handlebars.compile(content.toString());
|
||||
return template(params);
|
||||
};
|
||||
|
||||
/**
|
||||
* compile all hbs files under the specified directory. recursively
|
||||
*/
|
||||
export const compileDir = (dirPath: string, params: any) => {
|
||||
const files = readdirSync(dirPath, { recursive: true });
|
||||
|
||||
files.forEach((file: string | Buffer) => {
|
||||
// the target file should be a handlebars file
|
||||
if (typeof file !== "string" || !file.endsWith(".hbs")) return;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
48
packages/cli/src/utils/env/index.ts
vendored
Normal file
48
packages/cli/src/utils/env/index.ts
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { NODE_ENV } from "@definitions/node";
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
const refineEnv: Record<string, string> = {};
|
||||
|
||||
dotenv.config({ processEnv: refineEnv });
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const getEnvValue = (key: string): string | undefined => {
|
||||
return process.env[key] || refineEnv[key];
|
||||
};
|
||||
|
||||
export const ENV = {
|
||||
NODE_ENV: getNodeEnv(),
|
||||
REFINE_NO_TELEMETRY: getEnvValue("REFINE_NO_TELEMETRY") || "false",
|
||||
UPDATE_NOTIFIER_IS_DISABLED:
|
||||
getEnvValue("UPDATE_NOTIFIER_IS_DISABLED") || "false",
|
||||
UPDATE_NOTIFIER_CACHE_TTL:
|
||||
getEnvValue("UPDATE_NOTIFIER_CACHE_TTL") || 1000 * 60 * 60 * 24,
|
||||
REFINE_DEVTOOLS_PORT: getEnvValue("REFINE_DEVTOOLS_PORT"),
|
||||
};
|
||||
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);
|
||||
});
|
||||
});
|
||||
347
packages/cli/src/utils/package/index.ts
Normal file
347
packages/cli/src/utils/package/index.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
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";
|
||||
import type { PackageJson } from "@definitions/package";
|
||||
|
||||
export const getPackageJson = (): PackageJson => {
|
||||
if (!existsSync("package.json")) {
|
||||
console.error("❌ `package.json` not found.");
|
||||
throw new Error("./package.json not found");
|
||||
}
|
||||
|
||||
return JSON.parse(readFileSync("package.json", "utf8"));
|
||||
};
|
||||
|
||||
export const getDependencies = () => {
|
||||
const packageJson = getPackageJson();
|
||||
return Object.keys(packageJson.dependencies || {});
|
||||
};
|
||||
|
||||
export const getDependenciesWithVersion = () => {
|
||||
const packageJson = getPackageJson();
|
||||
return packageJson?.dependencies || {};
|
||||
};
|
||||
|
||||
export const getDevDependencies = () => {
|
||||
const packageJson = getPackageJson();
|
||||
return Object.keys(packageJson.devDependencies || {});
|
||||
};
|
||||
|
||||
export const getAllDependencies = () => {
|
||||
return [...getDependencies(), ...getDependencies()];
|
||||
};
|
||||
|
||||
export const getScripts = () => {
|
||||
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: {
|
||||
add: ["install", "--save"],
|
||||
addDev: ["install", "--save-dev"],
|
||||
outdatedJson: ["outdated", "--json"],
|
||||
install: ["install"],
|
||||
},
|
||||
yarn: {
|
||||
add: ["add"],
|
||||
addDev: ["add", "-D"],
|
||||
outdatedJson: ["outdated", "--json"],
|
||||
install: ["install"],
|
||||
},
|
||||
pnpm: {
|
||||
add: ["add"],
|
||||
addDev: ["add", "-D"],
|
||||
outdatedJson: ["outdated", "--format", "json"],
|
||||
install: ["install"],
|
||||
},
|
||||
bun: {
|
||||
add: ["add"],
|
||||
addDev: ["add", "--dev"],
|
||||
outdatedJson: ["outdated", "--format", "json"],
|
||||
install: ["install"],
|
||||
},
|
||||
};
|
||||
|
||||
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[],
|
||||
type: "all" | "add" = "all",
|
||||
successMessage = "All `Refine` packages updated 🎉",
|
||||
) => {
|
||||
const pm = await getPreferedPM();
|
||||
|
||||
try {
|
||||
const installCommand =
|
||||
type === "all" ? pmCommands[pm.name].install : pmCommands[pm.name].add;
|
||||
|
||||
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(successMessage);
|
||||
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].add;
|
||||
|
||||
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");
|
||||
};
|
||||
|
||||
export const getNotInstalledPackages = (packages: string[]) => {
|
||||
const dependencies = getDependencies();
|
||||
|
||||
return packages.filter((pkg) => !dependencies.includes(pkg));
|
||||
};
|
||||
|
||||
export const installMissingPackages = async (packages: string[]) => {
|
||||
console.log("🌱 Checking dependencies...");
|
||||
|
||||
const missingPackages = getNotInstalledPackages(packages);
|
||||
|
||||
if (missingPackages.length > 0) {
|
||||
console.log(`🌱 Installing ${missingPackages.join(", ")}`);
|
||||
|
||||
await installPackagesSync(missingPackages);
|
||||
|
||||
console.log("🎉 Installation complete...");
|
||||
} else {
|
||||
console.log("🎉 All required packages are already installed");
|
||||
}
|
||||
};
|
||||
|
||||
export const hasIncomatiblePackages = (packages: string[]): boolean => {
|
||||
const allDependencies = getAllDependencies();
|
||||
|
||||
const incompatiblePackages = packages.filter((pkg) =>
|
||||
allDependencies.includes(pkg),
|
||||
);
|
||||
|
||||
if (incompatiblePackages.length > 0) {
|
||||
console.log(
|
||||
`🚨 This feature doesn't support ${incompatiblePackages.join(
|
||||
", ",
|
||||
)} package.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getAllVersionsOfPackage = async (
|
||||
packageName: string,
|
||||
): Promise<string[]> => {
|
||||
const pm = "npm";
|
||||
|
||||
const { stdout, timedOut } = await execa(
|
||||
pm,
|
||||
["view", packageName, "versions", "--json"],
|
||||
{
|
||||
reject: false,
|
||||
timeout: 25 * 1000,
|
||||
},
|
||||
);
|
||||
|
||||
if (timedOut) {
|
||||
console.log("❌ Timed out while checking for updates.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let result:
|
||||
| string[]
|
||||
| {
|
||||
error: {
|
||||
code: string;
|
||||
};
|
||||
} = [];
|
||||
|
||||
try {
|
||||
result = JSON.parse(stdout);
|
||||
if (!result || "error" in result) {
|
||||
console.log("❌ Something went wrong while checking for updates.");
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("❌ Something went wrong while checking for updates.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const isInstalled = async (packageName: string) => {
|
||||
const installedPackages = await getInstalledRefinePackages();
|
||||
if (!installedPackages) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return installedPackages.some((pkg) => pkg.name === packageName);
|
||||
};
|
||||
104
packages/cli/src/utils/project/index.ts
Normal file
104
packages/cli/src/utils/project/index.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
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
|
||||
let dependencies: string[] = [];
|
||||
let devDependencies: string[] = [];
|
||||
|
||||
try {
|
||||
dependencies = getDependencies();
|
||||
devDependencies = getDevDependencies();
|
||||
} catch (error) {}
|
||||
|
||||
// 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")
|
||||
) {
|
||||
// check for remix-vite
|
||||
if (dependencies.includes("vite") || devDependencies.includes("vite")) {
|
||||
return ProjectTypes.REMIX_VITE;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export const getDevtoolsEnvKeyByProjectType = (
|
||||
projectType: ProjectTypes,
|
||||
): string => {
|
||||
switch (projectType) {
|
||||
case ProjectTypes.REACT_SCRIPT:
|
||||
return "REACT_APP_REFINE_DEVTOOLS_PORT";
|
||||
case ProjectTypes.NEXTJS:
|
||||
return "NEXT_PUBLIC_REFINE_DEVTOOLS_PORT";
|
||||
case ProjectTypes.VITE:
|
||||
return "VITE_REFINE_DEVTOOLS_PORT";
|
||||
default:
|
||||
return "REFINE_DEVTOOLS_PORT";
|
||||
}
|
||||
};
|
||||
69
packages/cli/src/utils/refine/index.test.tsx
Normal file
69
packages/cli/src/utils/refine/index.test.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
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({
|
||||
name: "test",
|
||||
version: "1.0.0",
|
||||
...testCase.input,
|
||||
});
|
||||
|
||||
const result = hasDefaultScript();
|
||||
|
||||
expect(result).toEqual(testCase.output);
|
||||
});
|
||||
});
|
||||
21
packages/cli/src/utils/refine/index.ts
Normal file
21
packages/cli/src/utils/refine/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
49
packages/cli/src/utils/resource/index.spec.ts
Normal file
49
packages/cli/src/utils/resource/index.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
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.REMIX_VITE)).toEqual({
|
||||
path: "app/providers",
|
||||
alias: "~/providers",
|
||||
});
|
||||
|
||||
expect(getProviderPath(ProjectTypes.REMIX_SPA)).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",
|
||||
});
|
||||
});
|
||||
70
packages/cli/src/utils/resource/index.ts
Normal file
70
packages/cli/src/utils/resource/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ProjectTypes } from "@definitions/projectTypes";
|
||||
import camelCase from "camelcase";
|
||||
|
||||
export const getResourcePath = (
|
||||
projectType: ProjectTypes,
|
||||
): { path: string; alias: string } => {
|
||||
switch (projectType) {
|
||||
case ProjectTypes.NEXTJS:
|
||||
return {
|
||||
path: "src/components",
|
||||
alias: "../src/components",
|
||||
};
|
||||
case ProjectTypes.REMIX:
|
||||
case ProjectTypes.REMIX_VITE:
|
||||
case ProjectTypes.REMIX_SPA:
|
||||
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:
|
||||
case ProjectTypes.REMIX_VITE:
|
||||
case ProjectTypes.REMIX_SPA:
|
||||
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:
|
||||
case ProjectTypes.REMIX_VITE:
|
||||
case ProjectTypes.REMIX_SPA:
|
||||
return "./app";
|
||||
default:
|
||||
return "./src";
|
||||
}
|
||||
};
|
||||
|
||||
export const getComponentNameByResource = (resource: string): string => {
|
||||
return camelCase(resource, {
|
||||
preserveConsecutiveUppercase: true,
|
||||
pascalCase: true,
|
||||
});
|
||||
};
|
||||
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");
|
||||
|
||||
`);
|
||||
});
|
||||
});
|
||||
17
packages/cli/src/utils/swizzle/appendAfterImports.ts
Normal file
17
packages/cli/src/utils/swizzle/appendAfterImports.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
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 type { 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;
|
||||
};
|
||||
256
packages/cli/src/utils/swizzle/import.test.ts
Normal file
256
packages/cli/src/utils/swizzle/import.test.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
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";
|
||||
import type { IAuthProvider } from "@refinedev/core";
|
||||
import { type BaseRecord } from "@refinedev/core";
|
||||
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import React from "react";',
|
||||
importPath: "react",
|
||||
defaultImport: "React",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import { Button } from "antd";',
|
||||
importPath: "antd",
|
||||
namedImports: "{ Button }",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import { TextInput as AntTextInput } from "antd";',
|
||||
importPath: "antd",
|
||||
namedImports: "{ TextInput as AntTextInput }",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import * as Antd from "antd";',
|
||||
importPath: "antd",
|
||||
namespaceImport: "Antd",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import { Button as AntButton, TextInput } from "antd";',
|
||||
importPath: "antd",
|
||||
namedImports: "{ Button as AntButton, TextInput }",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement:
|
||||
'import { Button as AntButton, TextInput as AntTextInput } from "antd";',
|
||||
importPath: "antd",
|
||||
namedImports: "{ Button as AntButton, TextInput as AntTextInput }",
|
||||
},
|
||||
{
|
||||
isType: true,
|
||||
statement: 'import type { IAuthProvider } from "@refinedev/core";',
|
||||
importPath: "@refinedev/core",
|
||||
namedImports: "{ IAuthProvider }",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import { type BaseRecord } from "@refinedev/core";',
|
||||
importPath: "@refinedev/core",
|
||||
namedImports: "{ type BaseRecord }",
|
||||
},
|
||||
];
|
||||
|
||||
expect(getImports(content)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNameChangeInImport", () => {
|
||||
it("should get all name changes", () => {
|
||||
const statement = `
|
||||
{ Button as AntButton, TextInput as AntTextInput, type ButtonProps, type TextInputProps as AntTextInputProps }
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{
|
||||
statement: " Button as AntButton,",
|
||||
fromName: "Button",
|
||||
toName: "AntButton",
|
||||
afterCharacter: ",",
|
||||
},
|
||||
{
|
||||
statement: " TextInput as AntTextInput,",
|
||||
fromName: "TextInput",
|
||||
toName: "AntTextInput",
|
||||
afterCharacter: ",",
|
||||
},
|
||||
{
|
||||
afterCharacter: undefined,
|
||||
fromName: "type TextInputProps",
|
||||
statement: " type TextInputProps as AntTextInputProps ",
|
||||
toName: "AntTextInputProps",
|
||||
},
|
||||
];
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
it("should keep type imports with content", () => {
|
||||
const content = `
|
||||
import type { AxiosInstance } from "axios";
|
||||
import { stringify } from "query-string";
|
||||
import type { DataProvider } from "@refinedev/core";
|
||||
import { axiosInstance, generateSort, generateFilter } from "./utils";
|
||||
|
||||
type MethodTypes = "get" | "delete" | "head" | "options";
|
||||
type MethodTypesWithBody = "post" | "put" | "patch";
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
import { axiosInstance, generateSort, generateFilter } from "./utils";
|
||||
import { stringify } from "query-string";
|
||||
import type { AxiosInstance } from "axios";
|
||||
import type { DataProvider } from "@refinedev/core";
|
||||
|
||||
|
||||
|
||||
type MethodTypes = "get" | "delete" | "head" | "options";
|
||||
type MethodTypesWithBody = "post" | "put" | "patch";
|
||||
`;
|
||||
|
||||
expect(reorderImports(content).trim()).toEqual(expected.trim());
|
||||
});
|
||||
});
|
||||
266
packages/cli/src/utils/swizzle/import.ts
Normal file
266
packages/cli/src/utils/swizzle/import.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
const packageRegex =
|
||||
/import(?:\s+(type))?\s*(?:([^\s\{\},]+)\s*(?:,\s*)?)?(\{[^}]+\})?\s*(?:\*\s*as\s+([^\s\{\}]+)\s*)?from\s*['"]([^'"]+)['"];?/g;
|
||||
|
||||
const nameChangeRegex = /((?:\w|\s|_)*)( as )((?:\w|\s|_)*)( |,)?/g;
|
||||
|
||||
export type ImportMatch = {
|
||||
statement: string;
|
||||
importPath: string;
|
||||
defaultImport?: string;
|
||||
namedImports?: string;
|
||||
namespaceImport?: string;
|
||||
isType?: boolean;
|
||||
};
|
||||
|
||||
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,
|
||||
typePrefix,
|
||||
defaultImport,
|
||||
namedImports,
|
||||
namespaceImport,
|
||||
importPath,
|
||||
] = match;
|
||||
|
||||
imports.push({
|
||||
isType: typePrefix === "type",
|
||||
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.isType,
|
||||
);
|
||||
const typeImports = allImports.filter((importMatch) => importMatch.isType);
|
||||
|
||||
const importsWithBeforeContent: ImportMatch[] = [];
|
||||
const importsWithoutBeforeContent: ImportMatch[] = [];
|
||||
|
||||
// // remove all type imports
|
||||
typeImports.forEach((importMatch) => {
|
||||
newContent = newContent.replace(`${importMatch.statement}\n`, "");
|
||||
});
|
||||
|
||||
allModuleImports.forEach((importMatch) => {
|
||||
if (isImportHasBeforeContent(newContent, 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, "");
|
||||
});
|
||||
|
||||
// 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;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user