This commit is contained in:
Stefan Pejcic
2024-11-07 19:03:37 +01:00
parent c6df945ed5
commit 09f9f9502d
2472 changed files with 620417 additions and 0 deletions

11
packages/cli/.npmignore Normal file
View 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
View File

@@ -0,0 +1 @@
legacy-peer-deps=true

View 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

File diff suppressed because it is too large Load Diff

31
packages/cli/README.md Normal file
View 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&#8594](https://refine.dev/docs/)
[Step up to refine tutorials &#8594](https://refine.dev/docs/tutorial/introduction/index/)
## Install
```
npm install @refinedev/cli
```

View 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
View 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
View 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();

View 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");
});
});

View 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;

View 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,
},
]);
};

View File

@@ -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);

View File

@@ -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,
});
},
};

View File

@@ -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,
];

View File

@@ -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,
});
},
};

View File

@@ -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",
},
]);
};

View File

@@ -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!`);
};

View File

@@ -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}`,
};
};

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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(),
},
]);
};

View File

@@ -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",
},
];

View File

@@ -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);

View File

@@ -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!",
);
}
};

View File

@@ -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 });
},
);
});

View 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);
});
});

View 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;

View 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;

View 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;

View File

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

View File

@@ -0,0 +1,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;

View File

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

View File

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

View 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";
},
},
};

View 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;

View 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;
};

View 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;

View 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(
", ",
)}`;
};

View 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;

View File

@@ -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:",
);
});
});

View File

@@ -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));
};

View 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;

View File

@@ -0,0 +1,117 @@
import { createUIGroup, validatePrompt } from ".";
test("Validate interactive prompt", () => {
const testCases = [
{
input: [
"@refinedev/airtable@1.7.8",
"@refinedev/airtable@2.7.8",
"@refinedev/airtable@3.33.0",
"@refinedev/simple-rest@2.7.8",
"@refinedev/simple-rest@3.35.2",
"@refinedev/core@3.88.4",
],
output: `You can't update the same package more than once. Please choice one.\n Duplicates: @refinedev/airtable, @refinedev/simple-rest`,
},
{
input: [],
output: true,
},
];
testCases.forEach((testCase) => {
const result = validatePrompt(testCase.input);
expect(result).toEqual(testCase.output);
});
});
test("Categorize UI Group", () => {
const testCases = [
{
input: [] 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);
});
});

View 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 };
};

View 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;

View 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();
};

View File

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

View File

@@ -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);
}
});

View 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;
};

View 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;
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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;
};
};

View 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",
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
export * from "./definitions/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";

View 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) {}
};

View 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 };
};

View File

@@ -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;"
`;

View File

@@ -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;"
`;

View File

@@ -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();
},
);
});

View 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"]);
};

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();
},
);
});

View 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,
);
};

View 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();
}

View 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);
});

View 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";
};

View 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",
}),
);
};

View File

@@ -0,0 +1,24 @@
import { findDuplicates } from "@utils/array";
test("Find duplicates from array", () => {
const testCases = [
{
input: [],
output: [],
},
{
input: [1, 2, 3, 3, "3", "3"],
output: [3, "3"],
},
{
input: [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5],
output: [1, 2, 3, 4, 5],
},
];
testCases.forEach((testCase) => {
const result = findDuplicates(testCase.input);
expect(result).toEqual(testCase.output);
});
});

View File

@@ -0,0 +1,5 @@
export const findDuplicates = (arr: (string | number)[]) => {
const duplicates = arr.filter((item, index) => arr.indexOf(item) !== index);
const unique = new Set(duplicates);
return Array.from(unique);
};

View File

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

View 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);
});
};

View File

@@ -0,0 +1,14 @@
export const stringToBase64 = (str: string) => {
if (typeof btoa !== "undefined") {
return btoa(str);
}
return Buffer.from(str).toString("base64");
};
export const base64ToString = (base64: string) => {
if (typeof atob !== "undefined") {
return atob(base64);
}
return Buffer.from(base64, "base64").toString();
};

View File

@@ -0,0 +1,55 @@
import { getNodeEnv } from ".";
test("Get NODE_ENV", async () => {
const testCases = [
{
input: "development",
expected: "development",
},
{
input: "dev",
expected: "development",
},
{
input: "Production",
expected: "production",
},
{
input: "prod",
expected: "production",
},
{
input: "test",
expected: "test",
},
{
input: "TESTING",
expected: "test",
},
{
input: "ci",
expected: "continuous-integration",
},
{
input: "UAT",
expected: "user-acceptance-testing",
},
{
input: "SIT",
expected: "system-integration-testing",
},
{
input: "another-node-env",
expected: "custom",
},
{
input: "",
expected: "development",
},
];
for (const testCase of testCases) {
process.env.NODE_ENV = testCase.input;
expect(getNodeEnv()).toEqual(testCase.expected);
}
});

48
packages/cli/src/utils/env/index.ts vendored Normal file
View 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"),
};

View File

@@ -0,0 +1,9 @@
import { renderCodeMarkdown } from "@utils/swizzle/renderCodeMarkdown";
import { marked } from "marked";
import TerminalRenderer from "marked-terminal";
export const markedTerminalRenderer = (markdown: string) => {
return marked(markdown, {
renderer: new TerminalRenderer({ code: renderCodeMarkdown }) as any,
});
};

View File

@@ -0,0 +1,25 @@
import envinfo from "envinfo";
import os from "os";
export const getOSType = () => {
const osPlatform = os.type();
const types: Record<string, "macOS" | "Linux" | "Windows"> = {
Darwin: "macOS",
Linux: "Linux",
Windows_NT: "Windows",
};
return types[osPlatform];
};
export const getOS = async () => {
// returns as a ['OS', 'macOS Mojave 10.14.5']
const [_, OSInfo] =
(await envinfo.helpers.getOSInfo()) as unknown as string[];
return {
name: getOSType(),
version: OSInfo,
};
};

View File

@@ -0,0 +1,46 @@
import { parsePackageNameAndVersion } from "@utils/package";
test("Get package name and version from string", () => {
const testCases = [
{
input: "@refinedev/antd@2.36.2",
output: {
name: "@refinedev/antd",
version: "2.36.2",
},
},
{
input: "@owner/package_name@2.36.2_beta.1",
output: {
name: "@owner/package_name",
version: "2.36.2_beta.1",
},
},
{
input: "@owner/package-name",
output: {
name: "@owner/package-name",
version: null,
},
},
{
input: "owner/package-name",
output: {
name: "owner/package-name",
version: null,
},
},
{
input: "owner/package-name@3.2.1",
output: {
name: "owner/package-name",
version: "3.2.1",
},
},
];
testCases.forEach((testCase) => {
const result = parsePackageNameAndVersion(testCase.input);
expect(result).toEqual(testCase.output);
});
});

View File

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

View 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";
}
};

View 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);
});
});

View 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,
};
};

View 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",
});
});

View 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,
});
};

View File

@@ -0,0 +1,13 @@
import ora from "ora";
const spinner = async <T>(fn: () => Promise<T>, message: string) => {
const spinner = ora({
color: "cyan",
text: message,
}).start();
const result = await fn();
spinner.stop();
return result;
};
export default spinner;

View File

@@ -0,0 +1,23 @@
import { appendAfterImports } from "./appendAfterImports";
describe("appendAfterImports", () => {
it("should append after imports", () => {
const content = `
import { foo } from "bar";
import { bar } from "foo";
import { baz } from "baz";
`;
const append = `console.log("hello world");`;
const result = appendAfterImports(content, append);
expect(result).toEqual(`
import { foo } from "bar";
import { bar } from "foo";
import { baz } from "baz";
console.log("hello world");
`);
});
});

View File

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

View File

@@ -0,0 +1,8 @@
export const SWIZZLE_CODES = {
SUCCESS: "SUCCESS",
UNKNOWN_ERROR: "UNKNOWN_ERROR",
SOURCE_PATH_NOT_FOUND: "SOURCE_PATH_NOT_FOUND",
TARGET_PATH_NOT_FOUND: "TARGET_PATH_NOT_FOUND",
SOURCE_PATH_NOT_A_FILE: "SOURCE_PATH_NOT_A_FILE",
TARGET_ALREADY_EXISTS: "TARGET_ALREADY_EXISTS",
};

View File

@@ -0,0 +1,16 @@
import { readFileSync } from "fs-extra";
import { join } from "path";
export function getFileContent(
this: undefined | { absolutePackageDir?: string },
path: string,
): string | undefined {
if (!this?.absolutePackageDir) {
return undefined;
}
try {
return readFileSync(join(this.absolutePackageDir, path)).toString();
} catch (err) {
return undefined;
}
}

View File

@@ -0,0 +1,17 @@
import 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;
};

View 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());
});
});

View 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