mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
packages
This commit is contained in:
81
packages/cli/src/utils/announcement/index.tsx
Normal file
81
packages/cli/src/utils/announcement/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import matter from "gray-matter";
|
||||
import boxen from "boxen";
|
||||
|
||||
import type { Announcement } from "@definitions/announcement";
|
||||
import { markedTerminalRenderer } from "@utils/marked-terminal-renderer";
|
||||
|
||||
const ANNOUNCEMENT_URL =
|
||||
"https://raw.githubusercontent.com/refinedev/refine/master/packages/cli/ANNOUNCEMENTS.md";
|
||||
|
||||
const ANNOUNCEMENT_DELIMITER = "---announcement";
|
||||
|
||||
const splitAnnouncements = (feed: string) => {
|
||||
const sections = feed.split(ANNOUNCEMENT_DELIMITER);
|
||||
|
||||
return sections
|
||||
.slice(1)
|
||||
.map((section) => `${ANNOUNCEMENT_DELIMITER}${section}`);
|
||||
};
|
||||
|
||||
const parseAnnouncement = (raw: string): Announcement => {
|
||||
const fixed = raw.replace(ANNOUNCEMENT_DELIMITER, "---");
|
||||
const parsed = matter(fixed);
|
||||
const content = (
|
||||
parsed.content.length === 0
|
||||
? fixed.replace(/---/g, "")
|
||||
: parsed.content.replace(/---/g, "")
|
||||
).trim();
|
||||
|
||||
return {
|
||||
...parsed.data,
|
||||
content,
|
||||
} as Announcement;
|
||||
};
|
||||
|
||||
export const getAnnouncements = async () => {
|
||||
try {
|
||||
const response = await fetch(ANNOUNCEMENT_URL)
|
||||
.then((res) => res.text())
|
||||
.catch(() => "");
|
||||
|
||||
const announcements = splitAnnouncements(response).map((section) =>
|
||||
parseAnnouncement(section),
|
||||
);
|
||||
|
||||
return announcements;
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const printAnnouncements = async () => {
|
||||
const announcements = await getAnnouncements();
|
||||
|
||||
const visibleAnnouncements = announcements.filter(
|
||||
(a) => Boolean(a.hidden) === false,
|
||||
);
|
||||
|
||||
if (visibleAnnouncements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stringAnnouncements = visibleAnnouncements
|
||||
.map((a) => {
|
||||
const dash = visibleAnnouncements.length > 1 ? "— " : "";
|
||||
const content = markedTerminalRenderer(a.content);
|
||||
return `${dash}${content}`;
|
||||
})
|
||||
.join("")
|
||||
.trim();
|
||||
|
||||
console.log(
|
||||
boxen(stringAnnouncements, {
|
||||
padding: 1,
|
||||
margin: 0,
|
||||
borderStyle: "round",
|
||||
borderColor: "blueBright",
|
||||
titleAlignment: "left",
|
||||
}),
|
||||
);
|
||||
};
|
||||
24
packages/cli/src/utils/array/index.test.ts
Normal file
24
packages/cli/src/utils/array/index.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { findDuplicates } from "@utils/array";
|
||||
|
||||
test("Find duplicates from array", () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: [],
|
||||
output: [],
|
||||
},
|
||||
|
||||
{
|
||||
input: [1, 2, 3, 3, "3", "3"],
|
||||
output: [3, "3"],
|
||||
},
|
||||
{
|
||||
input: [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5],
|
||||
output: [1, 2, 3, 4, 5],
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
const result = findDuplicates(testCase.input);
|
||||
expect(result).toEqual(testCase.output);
|
||||
});
|
||||
});
|
||||
5
packages/cli/src/utils/array/index.ts
Normal file
5
packages/cli/src/utils/array/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const findDuplicates = (arr: (string | number)[]) => {
|
||||
const duplicates = arr.filter((item, index) => arr.indexOf(item) !== index);
|
||||
const unique = new Set(duplicates);
|
||||
return Array.from(unique);
|
||||
};
|
||||
193
packages/cli/src/utils/codeshift/index.ts
Normal file
193
packages/cli/src/utils/codeshift/index.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import type {
|
||||
ASTPath,
|
||||
Collection,
|
||||
ImportDeclaration,
|
||||
JSCodeshift,
|
||||
JSXAttribute,
|
||||
JSXElement,
|
||||
JSXExpressionContainer,
|
||||
} from "jscodeshift";
|
||||
|
||||
export const wrapElement = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
element: ASTPath<JSXElement>,
|
||||
wrapper: string,
|
||||
wrapperAttributes: JSXAttribute[] = [],
|
||||
) => {
|
||||
const existingWrapperElement = source.find(j.JSXElement, {
|
||||
openingElement: { name: { name: wrapper } },
|
||||
});
|
||||
|
||||
if (existingWrapperElement.length) {
|
||||
return element;
|
||||
}
|
||||
|
||||
const wrapperElement = j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier(wrapper), wrapperAttributes),
|
||||
j.jsxClosingElement(j.jsxIdentifier(wrapper)),
|
||||
[
|
||||
j.jsxElement(
|
||||
element.node.openingElement,
|
||||
element.node.closingElement,
|
||||
element.node.children,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
j(element).replaceWith(wrapperElement);
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
export const wrapChildren = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
element: ASTPath<JSXElement>,
|
||||
wrapper: string,
|
||||
wrapperAttributes: JSXAttribute[] = [],
|
||||
) => {
|
||||
const existingWrapperElement = source.find(j.JSXElement, {
|
||||
openingElement: { name: { name: wrapper } },
|
||||
});
|
||||
|
||||
if (existingWrapperElement.length) {
|
||||
return element;
|
||||
}
|
||||
|
||||
const wrapperElement = j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier(wrapper), wrapperAttributes),
|
||||
j.jsxClosingElement(j.jsxIdentifier(wrapper)),
|
||||
element.value.children,
|
||||
);
|
||||
|
||||
j(element).replaceWith(
|
||||
j.jsxElement(element.node.openingElement, element.node.closingElement, [
|
||||
wrapperElement,
|
||||
]),
|
||||
);
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
export const addAttributeIfNotExist = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
element: ASTPath<JSXElement>,
|
||||
attributeIdentifier: string,
|
||||
attributeValue?: JSXElement | JSXExpressionContainer,
|
||||
) => {
|
||||
const existingAttribute = source.find(j.JSXAttribute, {
|
||||
name: {
|
||||
name: attributeIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingAttribute.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attribute = j.jsxAttribute(
|
||||
j.jsxIdentifier(attributeIdentifier),
|
||||
attributeValue ? attributeValue : undefined,
|
||||
);
|
||||
|
||||
element.node.openingElement.attributes?.push(attribute);
|
||||
};
|
||||
|
||||
export const addOrUpdateImports = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
importPath: string,
|
||||
importIdentifierNames: string[],
|
||||
insertFunc?: (
|
||||
sourceDeclaration: Collection<ImportDeclaration>,
|
||||
targetDeclaration: ImportDeclaration,
|
||||
) => void,
|
||||
isDefault = false,
|
||||
) => {
|
||||
const existingImports = source.find(j.ImportDeclaration, {
|
||||
source: {
|
||||
value: importPath,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefault && existingImports.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const specifierFunc = isDefault
|
||||
? j.importDefaultSpecifier
|
||||
: j.importSpecifier;
|
||||
|
||||
const importSpecifiers = importIdentifierNames.map((importIdentifierName) =>
|
||||
specifierFunc(j.identifier(importIdentifierName)),
|
||||
);
|
||||
|
||||
if (existingImports.length) {
|
||||
// Check existing imports in the `ImportDeclaration` to avoid duplicate imports
|
||||
const nonExistingImportIdentifiers = importIdentifierNames.filter(
|
||||
(importIdentifierName) =>
|
||||
existingImports.find(j.ImportSpecifier).filter((path) => {
|
||||
return path.node.imported.name === importIdentifierName.split(" ")[0];
|
||||
}).length === 0,
|
||||
);
|
||||
|
||||
if (nonExistingImportIdentifiers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nonExistingSpecifiers = nonExistingImportIdentifiers.map(
|
||||
(importIdentifierName) =>
|
||||
specifierFunc(j.identifier(importIdentifierName)),
|
||||
);
|
||||
|
||||
existingImports
|
||||
.at(0)
|
||||
.get("specifiers")
|
||||
.value.push(...nonExistingSpecifiers);
|
||||
} else {
|
||||
const importDeclaration = j.importDeclaration(
|
||||
importSpecifiers,
|
||||
j.literal(importPath),
|
||||
);
|
||||
|
||||
insertFunc?.(source.find(j.ImportDeclaration), importDeclaration);
|
||||
}
|
||||
};
|
||||
|
||||
export const addOrUpdateNamelessImport = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
importPath: string,
|
||||
insertFunc: (
|
||||
sourceDeclaration: Collection<ImportDeclaration>,
|
||||
targetDeclaration: ImportDeclaration,
|
||||
) => void,
|
||||
) => {
|
||||
const existingImports = source.find(j.ImportDeclaration, {
|
||||
source: {
|
||||
value: importPath,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingImports.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importDeclaration = j.importDeclaration([], j.literal(importPath));
|
||||
|
||||
insertFunc(source.find(j.ImportDeclaration), importDeclaration);
|
||||
};
|
||||
|
||||
export const removeImportIfExists = (
|
||||
j: JSCodeshift,
|
||||
source: Collection,
|
||||
importPath: string,
|
||||
importIdentifierName: string,
|
||||
) => {
|
||||
source
|
||||
.find(j.ImportDeclaration, { source: { value: importPath } })
|
||||
.find(j.ImportSpecifier, { imported: { name: importIdentifierName } })
|
||||
.remove();
|
||||
};
|
||||
76
packages/cli/src/utils/compile/index.ts
Normal file
76
packages/cli/src/utils/compile/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import Handlebars from "handlebars";
|
||||
import {
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
createFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from "fs-extra";
|
||||
import { getComponentNameByResource } from "@utils/resource";
|
||||
|
||||
export const compile = (filePath: string, params: any): string => {
|
||||
const content = readFileSync(filePath);
|
||||
|
||||
Handlebars.registerHelper("ifIn", (elem, list, options) => {
|
||||
if (elem.includes(list)) {
|
||||
return options.fn(Handlebars);
|
||||
}
|
||||
return options.inverse(Handlebars);
|
||||
});
|
||||
|
||||
Handlebars.registerHelper("formatInferencerComponent", (string) => {
|
||||
if (!string) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (string) {
|
||||
case "chakra-ui":
|
||||
return "ChakraUI";
|
||||
|
||||
default:
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
});
|
||||
|
||||
Handlebars.registerHelper("capitalize", (string) => {
|
||||
if (!string) {
|
||||
return;
|
||||
}
|
||||
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
});
|
||||
|
||||
Handlebars.registerHelper("getComponentNameByResource", (string) => {
|
||||
if (!string) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getComponentNameByResource(string);
|
||||
});
|
||||
|
||||
const template = Handlebars.compile(content.toString());
|
||||
return template(params);
|
||||
};
|
||||
|
||||
/**
|
||||
* compile all hbs files under the specified directory. recursively
|
||||
*/
|
||||
export const compileDir = (dirPath: string, params: any) => {
|
||||
const files = readdirSync(dirPath, { recursive: true });
|
||||
|
||||
files.forEach((file: string | Buffer) => {
|
||||
// the target file should be a handlebars file
|
||||
if (typeof file !== "string" || !file.endsWith(".hbs")) return;
|
||||
|
||||
const templateFilePath = `${dirPath}/${file}`;
|
||||
// create file
|
||||
const compiledFilePath = `${dirPath}/${file.replace(".hbs", "")}`;
|
||||
createFileSync(compiledFilePath);
|
||||
|
||||
// write compiled file
|
||||
writeFileSync(compiledFilePath, compile(templateFilePath, params));
|
||||
|
||||
// delete template file (*.hbs)
|
||||
unlinkSync(templateFilePath);
|
||||
});
|
||||
};
|
||||
14
packages/cli/src/utils/encode/index.ts
Normal file
14
packages/cli/src/utils/encode/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const stringToBase64 = (str: string) => {
|
||||
if (typeof btoa !== "undefined") {
|
||||
return btoa(str);
|
||||
}
|
||||
|
||||
return Buffer.from(str).toString("base64");
|
||||
};
|
||||
|
||||
export const base64ToString = (base64: string) => {
|
||||
if (typeof atob !== "undefined") {
|
||||
return atob(base64);
|
||||
}
|
||||
return Buffer.from(base64, "base64").toString();
|
||||
};
|
||||
55
packages/cli/src/utils/env/index.test.tsx
vendored
Normal file
55
packages/cli/src/utils/env/index.test.tsx
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
import { getNodeEnv } from ".";
|
||||
|
||||
test("Get NODE_ENV", async () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: "development",
|
||||
expected: "development",
|
||||
},
|
||||
{
|
||||
input: "dev",
|
||||
expected: "development",
|
||||
},
|
||||
{
|
||||
input: "Production",
|
||||
expected: "production",
|
||||
},
|
||||
{
|
||||
input: "prod",
|
||||
expected: "production",
|
||||
},
|
||||
{
|
||||
input: "test",
|
||||
expected: "test",
|
||||
},
|
||||
{
|
||||
input: "TESTING",
|
||||
expected: "test",
|
||||
},
|
||||
{
|
||||
input: "ci",
|
||||
expected: "continuous-integration",
|
||||
},
|
||||
{
|
||||
input: "UAT",
|
||||
expected: "user-acceptance-testing",
|
||||
},
|
||||
{
|
||||
input: "SIT",
|
||||
expected: "system-integration-testing",
|
||||
},
|
||||
{
|
||||
input: "another-node-env",
|
||||
expected: "custom",
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
expected: "development",
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
process.env.NODE_ENV = testCase.input;
|
||||
expect(getNodeEnv()).toEqual(testCase.expected);
|
||||
}
|
||||
});
|
||||
48
packages/cli/src/utils/env/index.ts
vendored
Normal file
48
packages/cli/src/utils/env/index.ts
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { NODE_ENV } from "@definitions/node";
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
const refineEnv: Record<string, string> = {};
|
||||
|
||||
dotenv.config({ processEnv: refineEnv });
|
||||
|
||||
const envSearchMap: Record<Exclude<NODE_ENV, "custom">, RegExp> = {
|
||||
development: /dev/i,
|
||||
production: /prod/i,
|
||||
test: /test|tst/i,
|
||||
"continuous-integration": /ci/i,
|
||||
"user-acceptance-testing": /uat/i,
|
||||
"system-integration-testing": /sit/i,
|
||||
};
|
||||
|
||||
export const getNodeEnv = (): NODE_ENV => {
|
||||
const nodeEnv = process.env.NODE_ENV;
|
||||
|
||||
if (!nodeEnv) {
|
||||
return "development";
|
||||
}
|
||||
|
||||
let env: NODE_ENV = "custom";
|
||||
|
||||
for (const [key, value] of Object.entries(envSearchMap)) {
|
||||
if (value.test(nodeEnv)) {
|
||||
env = key as NODE_ENV;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return env;
|
||||
};
|
||||
|
||||
const getEnvValue = (key: string): string | undefined => {
|
||||
return process.env[key] || refineEnv[key];
|
||||
};
|
||||
|
||||
export const ENV = {
|
||||
NODE_ENV: getNodeEnv(),
|
||||
REFINE_NO_TELEMETRY: getEnvValue("REFINE_NO_TELEMETRY") || "false",
|
||||
UPDATE_NOTIFIER_IS_DISABLED:
|
||||
getEnvValue("UPDATE_NOTIFIER_IS_DISABLED") || "false",
|
||||
UPDATE_NOTIFIER_CACHE_TTL:
|
||||
getEnvValue("UPDATE_NOTIFIER_CACHE_TTL") || 1000 * 60 * 60 * 24,
|
||||
REFINE_DEVTOOLS_PORT: getEnvValue("REFINE_DEVTOOLS_PORT"),
|
||||
};
|
||||
9
packages/cli/src/utils/marked-terminal-renderer/index.ts
Normal file
9
packages/cli/src/utils/marked-terminal-renderer/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { renderCodeMarkdown } from "@utils/swizzle/renderCodeMarkdown";
|
||||
import { marked } from "marked";
|
||||
import TerminalRenderer from "marked-terminal";
|
||||
|
||||
export const markedTerminalRenderer = (markdown: string) => {
|
||||
return marked(markdown, {
|
||||
renderer: new TerminalRenderer({ code: renderCodeMarkdown }) as any,
|
||||
});
|
||||
};
|
||||
25
packages/cli/src/utils/os/index.ts
Normal file
25
packages/cli/src/utils/os/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import envinfo from "envinfo";
|
||||
import os from "os";
|
||||
|
||||
export const getOSType = () => {
|
||||
const osPlatform = os.type();
|
||||
|
||||
const types: Record<string, "macOS" | "Linux" | "Windows"> = {
|
||||
Darwin: "macOS",
|
||||
Linux: "Linux",
|
||||
Windows_NT: "Windows",
|
||||
};
|
||||
|
||||
return types[osPlatform];
|
||||
};
|
||||
|
||||
export const getOS = async () => {
|
||||
// returns as a ['OS', 'macOS Mojave 10.14.5']
|
||||
const [_, OSInfo] =
|
||||
(await envinfo.helpers.getOSInfo()) as unknown as string[];
|
||||
|
||||
return {
|
||||
name: getOSType(),
|
||||
version: OSInfo,
|
||||
};
|
||||
};
|
||||
46
packages/cli/src/utils/package/index.test.tsx
Normal file
46
packages/cli/src/utils/package/index.test.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { parsePackageNameAndVersion } from "@utils/package";
|
||||
|
||||
test("Get package name and version from string", () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: "@refinedev/antd@2.36.2",
|
||||
output: {
|
||||
name: "@refinedev/antd",
|
||||
version: "2.36.2",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "@owner/package_name@2.36.2_beta.1",
|
||||
output: {
|
||||
name: "@owner/package_name",
|
||||
version: "2.36.2_beta.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "@owner/package-name",
|
||||
output: {
|
||||
name: "@owner/package-name",
|
||||
version: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "owner/package-name",
|
||||
output: {
|
||||
name: "owner/package-name",
|
||||
version: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "owner/package-name@3.2.1",
|
||||
output: {
|
||||
name: "owner/package-name",
|
||||
version: "3.2.1",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
const result = parsePackageNameAndVersion(testCase.input);
|
||||
expect(result).toEqual(testCase.output);
|
||||
});
|
||||
});
|
||||
347
packages/cli/src/utils/package/index.ts
Normal file
347
packages/cli/src/utils/package/index.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
import spinner from "@utils/spinner";
|
||||
import execa from "execa";
|
||||
import { existsSync, pathExists, readFileSync, readJSON } from "fs-extra";
|
||||
import globby from "globby";
|
||||
import path from "path";
|
||||
import preferredPM from "preferred-pm";
|
||||
import type { PackageJson } from "@definitions/package";
|
||||
|
||||
export const getPackageJson = (): PackageJson => {
|
||||
if (!existsSync("package.json")) {
|
||||
console.error("❌ `package.json` not found.");
|
||||
throw new Error("./package.json not found");
|
||||
}
|
||||
|
||||
return JSON.parse(readFileSync("package.json", "utf8"));
|
||||
};
|
||||
|
||||
export const getDependencies = () => {
|
||||
const packageJson = getPackageJson();
|
||||
return Object.keys(packageJson.dependencies || {});
|
||||
};
|
||||
|
||||
export const getDependenciesWithVersion = () => {
|
||||
const packageJson = getPackageJson();
|
||||
return packageJson?.dependencies || {};
|
||||
};
|
||||
|
||||
export const getDevDependencies = () => {
|
||||
const packageJson = getPackageJson();
|
||||
return Object.keys(packageJson.devDependencies || {});
|
||||
};
|
||||
|
||||
export const getAllDependencies = () => {
|
||||
return [...getDependencies(), ...getDependencies()];
|
||||
};
|
||||
|
||||
export const getScripts = () => {
|
||||
const packageJson = getPackageJson();
|
||||
return packageJson?.scripts || {};
|
||||
};
|
||||
|
||||
export const getInstalledRefinePackages = async () => {
|
||||
try {
|
||||
const execution = await execa("npm", ["ls", "--depth=0", "--json"], {
|
||||
reject: false,
|
||||
});
|
||||
|
||||
const dependencies = JSON.parse(execution.stdout)?.dependencies || {};
|
||||
const refineDependencies = Object.keys(dependencies).filter(
|
||||
(dependency) =>
|
||||
dependency.startsWith("@refinedev") ||
|
||||
dependency.startsWith("@pankod/refine-"),
|
||||
);
|
||||
|
||||
const normalize: {
|
||||
name: string;
|
||||
version: string;
|
||||
}[] = [];
|
||||
|
||||
for (const dependency of refineDependencies) {
|
||||
const version = dependencies[dependency].version;
|
||||
normalize.push({
|
||||
name: dependency,
|
||||
version,
|
||||
});
|
||||
}
|
||||
|
||||
return normalize;
|
||||
} catch (error) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
export const getInstalledRefinePackagesFromNodeModules = async () => {
|
||||
const REFINE_PACKAGES = ["core"];
|
||||
|
||||
try {
|
||||
const packagesFromGlobbySearch = await globby("node_modules/@refinedev/*", {
|
||||
onlyDirectories: true,
|
||||
});
|
||||
|
||||
const packageDirsFromModules = REFINE_PACKAGES.flatMap((pkg) => {
|
||||
try {
|
||||
const pkgPath = require.resolve(
|
||||
path.join("@refinedev", pkg, "package.json"),
|
||||
);
|
||||
|
||||
return [path.dirname(pkgPath)];
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const refinePackages: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
version: string;
|
||||
}> = [];
|
||||
|
||||
await Promise.all(
|
||||
[...packageDirsFromModules, ...packagesFromGlobbySearch].map(
|
||||
async (packageDir) => {
|
||||
const hasPackageJson = await pathExists(`${packageDir}/package.json`);
|
||||
if (hasPackageJson) {
|
||||
const packageJson = await readJSON(`${packageDir}/package.json`);
|
||||
|
||||
refinePackages.push({
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
path: packageDir,
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return refinePackages;
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const isPackageHaveRefineConfig = async (packagePath: string) => {
|
||||
return await pathExists(`${packagePath}/refine.config.js`);
|
||||
};
|
||||
|
||||
export const pmCommands = {
|
||||
npm: {
|
||||
add: ["install", "--save"],
|
||||
addDev: ["install", "--save-dev"],
|
||||
outdatedJson: ["outdated", "--json"],
|
||||
install: ["install"],
|
||||
},
|
||||
yarn: {
|
||||
add: ["add"],
|
||||
addDev: ["add", "-D"],
|
||||
outdatedJson: ["outdated", "--json"],
|
||||
install: ["install"],
|
||||
},
|
||||
pnpm: {
|
||||
add: ["add"],
|
||||
addDev: ["add", "-D"],
|
||||
outdatedJson: ["outdated", "--format", "json"],
|
||||
install: ["install"],
|
||||
},
|
||||
bun: {
|
||||
add: ["add"],
|
||||
addDev: ["add", "--dev"],
|
||||
outdatedJson: ["outdated", "--format", "json"],
|
||||
install: ["install"],
|
||||
},
|
||||
};
|
||||
|
||||
export const getPreferedPM = async () => {
|
||||
const pm = await spinner(
|
||||
() => preferredPM(process.cwd()),
|
||||
"Getting package manager...",
|
||||
);
|
||||
|
||||
if (!pm) {
|
||||
throw new Error("Package manager not found.");
|
||||
}
|
||||
|
||||
return pm;
|
||||
};
|
||||
|
||||
export const installPackages = async (
|
||||
packages: string[],
|
||||
type: "all" | "add" = "all",
|
||||
successMessage = "All `Refine` packages updated 🎉",
|
||||
) => {
|
||||
const pm = await getPreferedPM();
|
||||
|
||||
try {
|
||||
const installCommand =
|
||||
type === "all" ? pmCommands[pm.name].install : pmCommands[pm.name].add;
|
||||
|
||||
const execution = execa(pm.name, [...installCommand, ...packages], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
execution.on("message", (message) => {
|
||||
console.log(message);
|
||||
});
|
||||
|
||||
execution.on("error", (error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
execution.on("exit", (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
console.log(successMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Application exited with code ${exitCode}`);
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const installPackagesSync = async (packages: string[]) => {
|
||||
const pm = await getPreferedPM();
|
||||
|
||||
try {
|
||||
const installCommand = pmCommands[pm.name].add;
|
||||
|
||||
const execution = execa.sync(pm.name, [...installCommand, ...packages], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
if (execution.failed || execution.exitCode !== 0) {
|
||||
throw new Error(execution.stderr);
|
||||
}
|
||||
|
||||
return execution;
|
||||
} catch (error: any) {
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export interface PackageNameAndVersion {
|
||||
name: string;
|
||||
version: string | null;
|
||||
}
|
||||
|
||||
export const parsePackageNameAndVersion = (
|
||||
str: string,
|
||||
): PackageNameAndVersion => {
|
||||
const versionStartIndex = str.lastIndexOf("@");
|
||||
|
||||
if (versionStartIndex <= 0) {
|
||||
return {
|
||||
name: str,
|
||||
version: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: str.slice(0, versionStartIndex),
|
||||
version: str.slice(versionStartIndex + 1),
|
||||
};
|
||||
};
|
||||
|
||||
export const getRefineProjectId = () => {
|
||||
const packageJson = getPackageJson();
|
||||
|
||||
return packageJson?.refine?.projectId;
|
||||
};
|
||||
|
||||
export const isDevtoolsInstalled = async () => {
|
||||
const installedPackages = await getInstalledRefinePackagesFromNodeModules();
|
||||
|
||||
return installedPackages.some((pkg) => pkg.name === "@refinedev/devtools");
|
||||
};
|
||||
|
||||
export const getNotInstalledPackages = (packages: string[]) => {
|
||||
const dependencies = getDependencies();
|
||||
|
||||
return packages.filter((pkg) => !dependencies.includes(pkg));
|
||||
};
|
||||
|
||||
export const installMissingPackages = async (packages: string[]) => {
|
||||
console.log("🌱 Checking dependencies...");
|
||||
|
||||
const missingPackages = getNotInstalledPackages(packages);
|
||||
|
||||
if (missingPackages.length > 0) {
|
||||
console.log(`🌱 Installing ${missingPackages.join(", ")}`);
|
||||
|
||||
await installPackagesSync(missingPackages);
|
||||
|
||||
console.log("🎉 Installation complete...");
|
||||
} else {
|
||||
console.log("🎉 All required packages are already installed");
|
||||
}
|
||||
};
|
||||
|
||||
export const hasIncomatiblePackages = (packages: string[]): boolean => {
|
||||
const allDependencies = getAllDependencies();
|
||||
|
||||
const incompatiblePackages = packages.filter((pkg) =>
|
||||
allDependencies.includes(pkg),
|
||||
);
|
||||
|
||||
if (incompatiblePackages.length > 0) {
|
||||
console.log(
|
||||
`🚨 This feature doesn't support ${incompatiblePackages.join(
|
||||
", ",
|
||||
)} package.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getAllVersionsOfPackage = async (
|
||||
packageName: string,
|
||||
): Promise<string[]> => {
|
||||
const pm = "npm";
|
||||
|
||||
const { stdout, timedOut } = await execa(
|
||||
pm,
|
||||
["view", packageName, "versions", "--json"],
|
||||
{
|
||||
reject: false,
|
||||
timeout: 25 * 1000,
|
||||
},
|
||||
);
|
||||
|
||||
if (timedOut) {
|
||||
console.log("❌ Timed out while checking for updates.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let result:
|
||||
| string[]
|
||||
| {
|
||||
error: {
|
||||
code: string;
|
||||
};
|
||||
} = [];
|
||||
|
||||
try {
|
||||
result = JSON.parse(stdout);
|
||||
if (!result || "error" in result) {
|
||||
console.log("❌ Something went wrong while checking for updates.");
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("❌ Something went wrong while checking for updates.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const isInstalled = async (packageName: string) => {
|
||||
const installedPackages = await getInstalledRefinePackages();
|
||||
if (!installedPackages) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return installedPackages.some((pkg) => pkg.name === packageName);
|
||||
};
|
||||
104
packages/cli/src/utils/project/index.ts
Normal file
104
packages/cli/src/utils/project/index.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { ProjectTypes, UIFrameworks } from "@definitions";
|
||||
import { getDependencies, getDevDependencies } from "@utils/package";
|
||||
|
||||
export const getProjectType = (platform?: ProjectTypes): ProjectTypes => {
|
||||
if (platform) {
|
||||
return platform;
|
||||
}
|
||||
// read dependencies from package.json
|
||||
let dependencies: string[] = [];
|
||||
let devDependencies: string[] = [];
|
||||
|
||||
try {
|
||||
dependencies = getDependencies();
|
||||
devDependencies = getDevDependencies();
|
||||
} catch (error) {}
|
||||
|
||||
// check for craco
|
||||
// craco and react-scripts installs together. We need to check for craco first
|
||||
if (
|
||||
dependencies.includes("@craco/craco") ||
|
||||
devDependencies.includes("@craco/craco")
|
||||
) {
|
||||
return ProjectTypes.CRACO;
|
||||
}
|
||||
|
||||
// check for react-scripts
|
||||
if (
|
||||
dependencies.includes("react-scripts") ||
|
||||
devDependencies.includes("react-scripts")
|
||||
) {
|
||||
return ProjectTypes.REACT_SCRIPT;
|
||||
}
|
||||
|
||||
// check for next
|
||||
if (dependencies.includes("next") || devDependencies.includes("next")) {
|
||||
return ProjectTypes.NEXTJS;
|
||||
}
|
||||
|
||||
// check for remix
|
||||
if (
|
||||
dependencies.includes("@remix-run/react") ||
|
||||
devDependencies.includes("@remix-run/react")
|
||||
) {
|
||||
// check for remix-vite
|
||||
if (dependencies.includes("vite") || devDependencies.includes("vite")) {
|
||||
return ProjectTypes.REMIX_VITE;
|
||||
}
|
||||
|
||||
return ProjectTypes.REMIX;
|
||||
}
|
||||
|
||||
// check for vite
|
||||
if (dependencies.includes("vite") || devDependencies.includes("vite")) {
|
||||
return ProjectTypes.VITE;
|
||||
}
|
||||
|
||||
if (dependencies.includes("parcel") || devDependencies.includes("parcel")) {
|
||||
return ProjectTypes.PARCEL;
|
||||
}
|
||||
|
||||
return ProjectTypes.UNKNOWN;
|
||||
};
|
||||
|
||||
export const getUIFramework = (): UIFrameworks | undefined => {
|
||||
// read dependencies from package.json
|
||||
const dependencies = getDependencies();
|
||||
|
||||
// check for antd
|
||||
if (dependencies.includes("@refinedev/antd")) {
|
||||
return UIFrameworks.ANTD;
|
||||
}
|
||||
|
||||
// check for mui
|
||||
if (dependencies.includes("@refinedev/mui")) {
|
||||
return UIFrameworks.MUI;
|
||||
}
|
||||
|
||||
// check for chakra
|
||||
if (dependencies.includes("@refinedev/chakra-ui")) {
|
||||
return UIFrameworks.CHAKRA;
|
||||
}
|
||||
|
||||
// check for mantine
|
||||
if (dependencies.includes("@refinedev/mantine")) {
|
||||
return UIFrameworks.MANTINE;
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export const getDevtoolsEnvKeyByProjectType = (
|
||||
projectType: ProjectTypes,
|
||||
): string => {
|
||||
switch (projectType) {
|
||||
case ProjectTypes.REACT_SCRIPT:
|
||||
return "REACT_APP_REFINE_DEVTOOLS_PORT";
|
||||
case ProjectTypes.NEXTJS:
|
||||
return "NEXT_PUBLIC_REFINE_DEVTOOLS_PORT";
|
||||
case ProjectTypes.VITE:
|
||||
return "VITE_REFINE_DEVTOOLS_PORT";
|
||||
default:
|
||||
return "REFINE_DEVTOOLS_PORT";
|
||||
}
|
||||
};
|
||||
69
packages/cli/src/utils/refine/index.test.tsx
Normal file
69
packages/cli/src/utils/refine/index.test.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as utilsPackage from "@utils/package";
|
||||
import { hasDefaultScript } from ".";
|
||||
|
||||
test("Has default script", () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: {
|
||||
scripts: {
|
||||
dev: "refine dev",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
dev: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
scripts: {
|
||||
dev: "PORT=5252 refine dev --force",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
dev: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
scripts: {
|
||||
dev: "refine dev",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
dev: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
scripts: {
|
||||
dev: "refine dev2",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
dev: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
scripts: {
|
||||
dev: "refine dev;echo '1'",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
dev: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
jest.spyOn(utilsPackage, "getPackageJson").mockReturnValueOnce({
|
||||
name: "test",
|
||||
version: "1.0.0",
|
||||
...testCase.input,
|
||||
});
|
||||
|
||||
const result = hasDefaultScript();
|
||||
|
||||
expect(result).toEqual(testCase.output);
|
||||
});
|
||||
});
|
||||
21
packages/cli/src/utils/refine/index.ts
Normal file
21
packages/cli/src/utils/refine/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getPackageJson } from "@utils/package";
|
||||
|
||||
type ReturnType = {
|
||||
dev: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the project has a refine script in package.json
|
||||
*/
|
||||
export const hasDefaultScript = (): ReturnType => {
|
||||
const packageJson = getPackageJson();
|
||||
|
||||
const scripts = packageJson.scripts || {};
|
||||
|
||||
const isDefault =
|
||||
((scripts?.dev || "") as string).match(/refine dev(\s|$|;){1}/) !== null;
|
||||
|
||||
return {
|
||||
dev: isDefault,
|
||||
};
|
||||
};
|
||||
49
packages/cli/src/utils/resource/index.spec.ts
Normal file
49
packages/cli/src/utils/resource/index.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ProjectTypes } from "@definitions/projectTypes";
|
||||
import { getProviderPath } from ".";
|
||||
|
||||
it("should get provider path", () => {
|
||||
expect(getProviderPath(ProjectTypes.NEXTJS)).toEqual({
|
||||
path: "src/providers",
|
||||
alias: "../src/providers",
|
||||
});
|
||||
|
||||
expect(getProviderPath(ProjectTypes.REMIX)).toEqual({
|
||||
path: "app/providers",
|
||||
alias: "~/providers",
|
||||
});
|
||||
|
||||
expect(getProviderPath(ProjectTypes.REMIX_VITE)).toEqual({
|
||||
path: "app/providers",
|
||||
alias: "~/providers",
|
||||
});
|
||||
|
||||
expect(getProviderPath(ProjectTypes.REMIX_SPA)).toEqual({
|
||||
path: "app/providers",
|
||||
alias: "~/providers",
|
||||
});
|
||||
|
||||
expect(getProviderPath(ProjectTypes.VITE)).toEqual({
|
||||
path: "src/providers",
|
||||
alias: "providers",
|
||||
});
|
||||
|
||||
expect(getProviderPath(ProjectTypes.REACT_SCRIPT)).toEqual({
|
||||
path: "src/providers",
|
||||
alias: "providers",
|
||||
});
|
||||
|
||||
expect(getProviderPath(ProjectTypes.CRACO)).toEqual({
|
||||
path: "src/providers",
|
||||
alias: "providers",
|
||||
});
|
||||
|
||||
expect(getProviderPath(ProjectTypes.PARCEL)).toEqual({
|
||||
path: "src/providers",
|
||||
alias: "providers",
|
||||
});
|
||||
|
||||
expect(getProviderPath(ProjectTypes.UNKNOWN)).toEqual({
|
||||
path: "src/providers",
|
||||
alias: "providers",
|
||||
});
|
||||
});
|
||||
70
packages/cli/src/utils/resource/index.ts
Normal file
70
packages/cli/src/utils/resource/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ProjectTypes } from "@definitions/projectTypes";
|
||||
import camelCase from "camelcase";
|
||||
|
||||
export const getResourcePath = (
|
||||
projectType: ProjectTypes,
|
||||
): { path: string; alias: string } => {
|
||||
switch (projectType) {
|
||||
case ProjectTypes.NEXTJS:
|
||||
return {
|
||||
path: "src/components",
|
||||
alias: "../src/components",
|
||||
};
|
||||
case ProjectTypes.REMIX:
|
||||
case ProjectTypes.REMIX_VITE:
|
||||
case ProjectTypes.REMIX_SPA:
|
||||
return {
|
||||
path: "app/components",
|
||||
alias: "~/components",
|
||||
};
|
||||
}
|
||||
|
||||
// vite and react
|
||||
return {
|
||||
path: "src/pages",
|
||||
alias: "pages",
|
||||
};
|
||||
};
|
||||
|
||||
export const getProviderPath = (
|
||||
projectType: ProjectTypes,
|
||||
): { path: string; alias: string } => {
|
||||
switch (projectType) {
|
||||
case ProjectTypes.NEXTJS:
|
||||
return {
|
||||
path: "src/providers",
|
||||
alias: "../src/providers",
|
||||
};
|
||||
case ProjectTypes.REMIX:
|
||||
case ProjectTypes.REMIX_VITE:
|
||||
case ProjectTypes.REMIX_SPA:
|
||||
return {
|
||||
path: "app/providers",
|
||||
alias: "~/providers",
|
||||
};
|
||||
}
|
||||
|
||||
// vite and react
|
||||
return {
|
||||
path: "src/providers",
|
||||
alias: "providers",
|
||||
};
|
||||
};
|
||||
|
||||
export const getFilesPathByProject = (projectType?: ProjectTypes) => {
|
||||
switch (projectType) {
|
||||
case ProjectTypes.REMIX:
|
||||
case ProjectTypes.REMIX_VITE:
|
||||
case ProjectTypes.REMIX_SPA:
|
||||
return "./app";
|
||||
default:
|
||||
return "./src";
|
||||
}
|
||||
};
|
||||
|
||||
export const getComponentNameByResource = (resource: string): string => {
|
||||
return camelCase(resource, {
|
||||
preserveConsecutiveUppercase: true,
|
||||
pascalCase: true,
|
||||
});
|
||||
};
|
||||
13
packages/cli/src/utils/spinner/index.ts
Normal file
13
packages/cli/src/utils/spinner/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import ora from "ora";
|
||||
|
||||
const spinner = async <T>(fn: () => Promise<T>, message: string) => {
|
||||
const spinner = ora({
|
||||
color: "cyan",
|
||||
text: message,
|
||||
}).start();
|
||||
const result = await fn();
|
||||
spinner.stop();
|
||||
return result;
|
||||
};
|
||||
|
||||
export default spinner;
|
||||
23
packages/cli/src/utils/swizzle/appendAfterImports.test.ts
Normal file
23
packages/cli/src/utils/swizzle/appendAfterImports.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { appendAfterImports } from "./appendAfterImports";
|
||||
|
||||
describe("appendAfterImports", () => {
|
||||
it("should append after imports", () => {
|
||||
const content = `
|
||||
import { foo } from "bar";
|
||||
import { bar } from "foo";
|
||||
import { baz } from "baz";
|
||||
`;
|
||||
|
||||
const append = `console.log("hello world");`;
|
||||
|
||||
const result = appendAfterImports(content, append);
|
||||
|
||||
expect(result).toEqual(`
|
||||
import { foo } from "bar";
|
||||
import { bar } from "foo";
|
||||
import { baz } from "baz";
|
||||
console.log("hello world");
|
||||
|
||||
`);
|
||||
});
|
||||
});
|
||||
17
packages/cli/src/utils/swizzle/appendAfterImports.ts
Normal file
17
packages/cli/src/utils/swizzle/appendAfterImports.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getImports } from "./import";
|
||||
|
||||
export const appendAfterImports = (content: string, append: string): string => {
|
||||
const imports = getImports(content);
|
||||
const lastImport = imports[imports.length - 1];
|
||||
|
||||
const lastImportIndex = lastImport
|
||||
? content.indexOf(lastImport.statement)
|
||||
: content.length - 1;
|
||||
|
||||
return `${content.slice(
|
||||
0,
|
||||
lastImportIndex + lastImport?.statement.length,
|
||||
)}\n${append}\n${content.slice(
|
||||
lastImportIndex + lastImport?.statement.length,
|
||||
)}`;
|
||||
};
|
||||
8
packages/cli/src/utils/swizzle/codes.ts
Normal file
8
packages/cli/src/utils/swizzle/codes.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const SWIZZLE_CODES = {
|
||||
SUCCESS: "SUCCESS",
|
||||
UNKNOWN_ERROR: "UNKNOWN_ERROR",
|
||||
SOURCE_PATH_NOT_FOUND: "SOURCE_PATH_NOT_FOUND",
|
||||
TARGET_PATH_NOT_FOUND: "TARGET_PATH_NOT_FOUND",
|
||||
SOURCE_PATH_NOT_A_FILE: "SOURCE_PATH_NOT_A_FILE",
|
||||
TARGET_ALREADY_EXISTS: "TARGET_ALREADY_EXISTS",
|
||||
};
|
||||
16
packages/cli/src/utils/swizzle/getFileContent.ts
Normal file
16
packages/cli/src/utils/swizzle/getFileContent.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { readFileSync } from "fs-extra";
|
||||
import { join } from "path";
|
||||
|
||||
export function getFileContent(
|
||||
this: undefined | { absolutePackageDir?: string },
|
||||
path: string,
|
||||
): string | undefined {
|
||||
if (!this?.absolutePackageDir) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return readFileSync(join(this.absolutePackageDir, path)).toString();
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
17
packages/cli/src/utils/swizzle/getPathPrefix.ts
Normal file
17
packages/cli/src/utils/swizzle/getPathPrefix.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { ProjectTypes } from "@definitions/projectTypes";
|
||||
import { getProjectType } from "@utils/project";
|
||||
import { getFilesPathByProject } from "@utils/resource";
|
||||
|
||||
export const getPathPrefix = () => {
|
||||
let projectType: ProjectTypes | undefined = undefined;
|
||||
|
||||
try {
|
||||
projectType = getProjectType();
|
||||
} catch (error) {
|
||||
projectType = undefined;
|
||||
}
|
||||
|
||||
const pathPrefix = getFilesPathByProject(projectType);
|
||||
|
||||
return pathPrefix;
|
||||
};
|
||||
256
packages/cli/src/utils/swizzle/import.test.ts
Normal file
256
packages/cli/src/utils/swizzle/import.test.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import { getImports, getNameChangeInImport, reorderImports } from "./import";
|
||||
|
||||
describe("getImports", () => {
|
||||
it("should get all imports", () => {
|
||||
const content = `
|
||||
import React from "react";
|
||||
import { Button } from "antd";
|
||||
import { TextInput as AntTextInput } from "antd";
|
||||
import * as Antd from "antd";
|
||||
import { Button as AntButton, TextInput } from "antd";
|
||||
import { Button as AntButton, TextInput as AntTextInput } from "antd";
|
||||
import type { IAuthProvider } from "@refinedev/core";
|
||||
import { type BaseRecord } from "@refinedev/core";
|
||||
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import React from "react";',
|
||||
importPath: "react",
|
||||
defaultImport: "React",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import { Button } from "antd";',
|
||||
importPath: "antd",
|
||||
namedImports: "{ Button }",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import { TextInput as AntTextInput } from "antd";',
|
||||
importPath: "antd",
|
||||
namedImports: "{ TextInput as AntTextInput }",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import * as Antd from "antd";',
|
||||
importPath: "antd",
|
||||
namespaceImport: "Antd",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import { Button as AntButton, TextInput } from "antd";',
|
||||
importPath: "antd",
|
||||
namedImports: "{ Button as AntButton, TextInput }",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement:
|
||||
'import { Button as AntButton, TextInput as AntTextInput } from "antd";',
|
||||
importPath: "antd",
|
||||
namedImports: "{ Button as AntButton, TextInput as AntTextInput }",
|
||||
},
|
||||
{
|
||||
isType: true,
|
||||
statement: 'import type { IAuthProvider } from "@refinedev/core";',
|
||||
importPath: "@refinedev/core",
|
||||
namedImports: "{ IAuthProvider }",
|
||||
},
|
||||
{
|
||||
isType: false,
|
||||
statement: 'import { type BaseRecord } from "@refinedev/core";',
|
||||
importPath: "@refinedev/core",
|
||||
namedImports: "{ type BaseRecord }",
|
||||
},
|
||||
];
|
||||
|
||||
expect(getImports(content)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNameChangeInImport", () => {
|
||||
it("should get all name changes", () => {
|
||||
const statement = `
|
||||
{ Button as AntButton, TextInput as AntTextInput, type ButtonProps, type TextInputProps as AntTextInputProps }
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{
|
||||
statement: " Button as AntButton,",
|
||||
fromName: "Button",
|
||||
toName: "AntButton",
|
||||
afterCharacter: ",",
|
||||
},
|
||||
{
|
||||
statement: " TextInput as AntTextInput,",
|
||||
fromName: "TextInput",
|
||||
toName: "AntTextInput",
|
||||
afterCharacter: ",",
|
||||
},
|
||||
{
|
||||
afterCharacter: undefined,
|
||||
fromName: "type TextInputProps",
|
||||
statement: " type TextInputProps as AntTextInputProps ",
|
||||
toName: "AntTextInputProps",
|
||||
},
|
||||
];
|
||||
|
||||
expect(getNameChangeInImport(statement)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reorderImports", () => {
|
||||
it("should reorder named imports", () => {
|
||||
const content = `
|
||||
import { Button, TextInput } from "zantd";
|
||||
import { useEffect } from "react";
|
||||
import { useList } from "@refinedev/core";
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
import { useEffect } from "react";
|
||||
import { useList } from "@refinedev/core";
|
||||
import { Button, TextInput } from "zantd";
|
||||
`;
|
||||
|
||||
expect(reorderImports(content).trim()).toEqual(expected.trim());
|
||||
});
|
||||
|
||||
it("should merge the same module imports", () => {
|
||||
const content = `
|
||||
import { Button, TextInput } from "antd";
|
||||
import { useEffect } from "react";
|
||||
import { useList, useOtherList, } from "@refinedev/core";
|
||||
import { useOne, useOtherOne } from "@refinedev/core";
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
import { useEffect } from "react";
|
||||
import { useList, useOtherList, useOne, useOtherOne } from "@refinedev/core";
|
||||
import { Button, TextInput } from "antd";
|
||||
`;
|
||||
|
||||
expect(reorderImports(content).trim()).toEqual(expected.trim());
|
||||
});
|
||||
|
||||
it("should merge default imports with named imports", () => {
|
||||
const content = `
|
||||
import { Button, TextInput } from "antd";
|
||||
import { useEffect } from "react";
|
||||
import React from "react";
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
import React, { useEffect } from "react";
|
||||
import { Button, TextInput } from "antd";
|
||||
`;
|
||||
|
||||
expect(reorderImports(content).trim()).toEqual(expected.trim());
|
||||
});
|
||||
|
||||
it("should not merge namespace imports with named imports", () => {
|
||||
const content = `
|
||||
import { Button, TextInput } from "antd";
|
||||
import { useEffect } from "react";
|
||||
import * as Antd from "antd";
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
import { useEffect } from "react";
|
||||
import * as Antd from "antd";
|
||||
import { Button, TextInput } from "antd";
|
||||
`;
|
||||
|
||||
expect(reorderImports(content).trim()).toEqual(expected.trim());
|
||||
});
|
||||
|
||||
it("should not merge namespace imports with default imports", () => {
|
||||
const content = `
|
||||
import React from "react";
|
||||
import * as ReactPackage from "react";
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
import * as ReactPackage from "react";
|
||||
import React from "react";
|
||||
`;
|
||||
|
||||
expect(reorderImports(content).trim()).toEqual(expected.trim());
|
||||
});
|
||||
|
||||
it("should keep name changes in named imports", () => {
|
||||
const content = `
|
||||
import { Button, TextInput } from "antd";
|
||||
import { Layout as AntLayout } from "antd";
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
import { Button, TextInput, Layout as AntLayout } from "antd";
|
||||
`;
|
||||
|
||||
expect(reorderImports(content).trim()).toEqual(expected.trim());
|
||||
});
|
||||
|
||||
it("should keep the imports with comments before", () => {
|
||||
const content = `
|
||||
import React from "react";
|
||||
// comment
|
||||
import { Button, TextInput } from "antd";
|
||||
import { Layout } from "antd";
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
import React from "react";
|
||||
import { Layout } from "antd";
|
||||
|
||||
// comment
|
||||
import { Button, TextInput } from "antd";
|
||||
`;
|
||||
|
||||
expect(reorderImports(content).trim()).toEqual(expected.trim());
|
||||
});
|
||||
|
||||
it("should keep type imports and add them to the end", () => {
|
||||
const content = `
|
||||
import type { Layout } from "antd";
|
||||
import React from "react";
|
||||
import { Button, TextInput } from "antd";
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
import React from "react";
|
||||
import { Button, TextInput } from "antd";
|
||||
import type { Layout } from "antd";
|
||||
`;
|
||||
|
||||
expect(reorderImports(content).trim()).toEqual(expected.trim());
|
||||
});
|
||||
|
||||
it("should keep type imports with content", () => {
|
||||
const content = `
|
||||
import type { AxiosInstance } from "axios";
|
||||
import { stringify } from "query-string";
|
||||
import type { DataProvider } from "@refinedev/core";
|
||||
import { axiosInstance, generateSort, generateFilter } from "./utils";
|
||||
|
||||
type MethodTypes = "get" | "delete" | "head" | "options";
|
||||
type MethodTypesWithBody = "post" | "put" | "patch";
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
import { axiosInstance, generateSort, generateFilter } from "./utils";
|
||||
import { stringify } from "query-string";
|
||||
import type { AxiosInstance } from "axios";
|
||||
import type { DataProvider } from "@refinedev/core";
|
||||
|
||||
|
||||
|
||||
type MethodTypes = "get" | "delete" | "head" | "options";
|
||||
type MethodTypesWithBody = "post" | "put" | "patch";
|
||||
`;
|
||||
|
||||
expect(reorderImports(content).trim()).toEqual(expected.trim());
|
||||
});
|
||||
});
|
||||
266
packages/cli/src/utils/swizzle/import.ts
Normal file
266
packages/cli/src/utils/swizzle/import.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
const packageRegex =
|
||||
/import(?:\s+(type))?\s*(?:([^\s\{\},]+)\s*(?:,\s*)?)?(\{[^}]+\})?\s*(?:\*\s*as\s+([^\s\{\}]+)\s*)?from\s*['"]([^'"]+)['"];?/g;
|
||||
|
||||
const nameChangeRegex = /((?:\w|\s|_)*)( as )((?:\w|\s|_)*)( |,)?/g;
|
||||
|
||||
export type ImportMatch = {
|
||||
statement: string;
|
||||
importPath: string;
|
||||
defaultImport?: string;
|
||||
namedImports?: string;
|
||||
namespaceImport?: string;
|
||||
isType?: boolean;
|
||||
};
|
||||
|
||||
export type NameChangeMatch = {
|
||||
statement: string;
|
||||
fromName: string;
|
||||
toName: string;
|
||||
afterCharacter?: string;
|
||||
};
|
||||
|
||||
export const getImports = (content: string): Array<ImportMatch> => {
|
||||
const matches = content.matchAll(packageRegex);
|
||||
|
||||
const imports: Array<ImportMatch> = [];
|
||||
|
||||
for (const match of matches) {
|
||||
const [
|
||||
statement,
|
||||
typePrefix,
|
||||
defaultImport,
|
||||
namedImports,
|
||||
namespaceImport,
|
||||
importPath,
|
||||
] = match;
|
||||
|
||||
imports.push({
|
||||
isType: typePrefix === "type",
|
||||
statement,
|
||||
importPath,
|
||||
...(defaultImport && { defaultImport }),
|
||||
...(namedImports && { namedImports }),
|
||||
...(namespaceImport && { namespaceImport }),
|
||||
});
|
||||
}
|
||||
|
||||
return imports?.filter(Boolean);
|
||||
};
|
||||
|
||||
export const getNameChangeInImport = (
|
||||
namedImportString: string,
|
||||
): Array<NameChangeMatch> => {
|
||||
const matches = namedImportString.matchAll(nameChangeRegex);
|
||||
|
||||
const nameChanges: Array<NameChangeMatch> = [];
|
||||
|
||||
for (const match of matches) {
|
||||
const [statement, fromName, _as, toName, afterCharacter] = match;
|
||||
|
||||
nameChanges.push({
|
||||
statement,
|
||||
fromName: fromName.trim(),
|
||||
toName: toName.trim(),
|
||||
afterCharacter,
|
||||
});
|
||||
}
|
||||
|
||||
return nameChanges;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export const getContentBeforeImport = (
|
||||
content: string,
|
||||
importMatch: ImportMatch,
|
||||
): string => {
|
||||
// get the content before the import statement and between the last import statement and the current one
|
||||
const contentBeforeImport = content.substring(
|
||||
0,
|
||||
content.indexOf(importMatch.statement),
|
||||
);
|
||||
// get the last import statement
|
||||
const lastImportStatement = getImports(contentBeforeImport).pop();
|
||||
|
||||
// if there is no last import statement, return the content before the current import statement
|
||||
if (!lastImportStatement) {
|
||||
return contentBeforeImport;
|
||||
}
|
||||
|
||||
// get the content between the last import statement and the current one
|
||||
const contentBetweenImports = contentBeforeImport.substring(
|
||||
contentBeforeImport.indexOf(lastImportStatement?.statement) +
|
||||
lastImportStatement?.statement?.length,
|
||||
);
|
||||
|
||||
// return the content before the current import statement and between the last import statement and the current one
|
||||
return contentBetweenImports;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export const isImportHasBeforeContent = (
|
||||
content: string,
|
||||
importMatch: ImportMatch,
|
||||
): boolean => {
|
||||
const contentBeforeImport = importMatch
|
||||
? getContentBeforeImport(content, importMatch)
|
||||
: "";
|
||||
|
||||
return !!contentBeforeImport.trim();
|
||||
};
|
||||
|
||||
const IMPORT_ORDER = ["react", "@refinedev/core", "@refinedev/"];
|
||||
|
||||
export const reorderImports = (content: string): string => {
|
||||
let newContent = content;
|
||||
// imports can have comments before them, we need to preserve those comments and import statements.
|
||||
// so we need to filter out the imports with comments before.
|
||||
const allImports = getImports(content);
|
||||
// remove `import type` imports
|
||||
const allModuleImports = allImports.filter(
|
||||
(importMatch) => !importMatch.isType,
|
||||
);
|
||||
const typeImports = allImports.filter((importMatch) => importMatch.isType);
|
||||
|
||||
const importsWithBeforeContent: ImportMatch[] = [];
|
||||
const importsWithoutBeforeContent: ImportMatch[] = [];
|
||||
|
||||
// // remove all type imports
|
||||
typeImports.forEach((importMatch) => {
|
||||
newContent = newContent.replace(`${importMatch.statement}\n`, "");
|
||||
});
|
||||
|
||||
allModuleImports.forEach((importMatch) => {
|
||||
if (isImportHasBeforeContent(newContent, importMatch)) {
|
||||
importsWithBeforeContent.push(importMatch);
|
||||
} else {
|
||||
importsWithoutBeforeContent.push(importMatch);
|
||||
}
|
||||
});
|
||||
|
||||
// insertion point is the first import statement, others will be replaced to empty string and added to the first import line
|
||||
const insertionPoint = newContent.indexOf(
|
||||
importsWithoutBeforeContent?.[0]?.statement,
|
||||
);
|
||||
|
||||
// remove all the imports without comments before
|
||||
importsWithoutBeforeContent.forEach((importMatch) => {
|
||||
newContent = newContent.replace(importMatch.statement, "");
|
||||
});
|
||||
|
||||
// we need to merge the imports from the same package unless one of them is a namespace import]
|
||||
const importsByPackage = importsWithoutBeforeContent.reduce(
|
||||
(acc, importMatch) => {
|
||||
const { importPath } = importMatch;
|
||||
|
||||
if (acc[importPath]) {
|
||||
acc[importPath].push(importMatch);
|
||||
} else {
|
||||
acc[importPath] = [importMatch];
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ImportMatch[]>,
|
||||
);
|
||||
|
||||
// merge the imports from the same package
|
||||
const mergedImports = Object.entries(importsByPackage).map(
|
||||
([importPath, importMatches]) => {
|
||||
// example: A
|
||||
const defaultImport = importMatches.find(
|
||||
(importMatch) => importMatch.defaultImport,
|
||||
);
|
||||
|
||||
// example: * as A
|
||||
const namespaceImport = importMatches.find(
|
||||
(importMatch) => importMatch.namespaceImport,
|
||||
);
|
||||
|
||||
// example: { A, B }
|
||||
// example: { A as C, B }
|
||||
// content inside the curly braces should be merged
|
||||
const namedImports = importMatches
|
||||
.filter((importMatch) => importMatch.namedImports)
|
||||
.map((importMatch) => {
|
||||
// remove curly braces and trim then split by comma (can be multiline)
|
||||
const namedImports = (importMatch.namedImports ?? "")
|
||||
.replace(/{|}/g, "")
|
||||
.trim()
|
||||
.split(",")
|
||||
.map((namedImport) => namedImport.trim());
|
||||
|
||||
return namedImports.filter(Boolean).join(", ");
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
let importLine = "";
|
||||
|
||||
// default import and namespace import can not be used together
|
||||
// but we can use default import and named imports together
|
||||
// so we need to merge them
|
||||
if (namespaceImport) {
|
||||
importLine += `${namespaceImport.statement}\n`;
|
||||
}
|
||||
if (defaultImport || namedImports) {
|
||||
if (defaultImport && namedImports) {
|
||||
importLine += `import ${defaultImport.defaultImport}, { ${namedImports} } from "${importMatches[0].importPath}";\n`;
|
||||
} else if (defaultImport) {
|
||||
importLine += `import ${defaultImport.defaultImport} from "${importMatches[0].importPath}";\n`;
|
||||
} else {
|
||||
importLine += `import { ${namedImports} } from "${importMatches[0].importPath}";\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return [importPath, importLine] as [
|
||||
importPath: string,
|
||||
importLine: string,
|
||||
];
|
||||
},
|
||||
);
|
||||
|
||||
// sort the imports without comments before
|
||||
// sort should be done by IMPORT_ORDER and alphabetically
|
||||
// priority is exact match in IMPORT_ORDER, then includes match in IMPORT_ORDER, then alphabetically
|
||||
const sortedImports = [...mergedImports].sort(
|
||||
([aImportPath], [bImportPath]) => {
|
||||
const aImportOrderIndex = IMPORT_ORDER.findIndex((order) =>
|
||||
aImportPath.includes(order),
|
||||
);
|
||||
const bImportOrderIndex = IMPORT_ORDER.findIndex((order) =>
|
||||
bImportPath.includes(order),
|
||||
);
|
||||
|
||||
if (aImportOrderIndex === bImportOrderIndex) {
|
||||
return aImportPath.localeCompare(bImportPath);
|
||||
}
|
||||
|
||||
if (aImportOrderIndex === -1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (bImportOrderIndex === -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return aImportOrderIndex - bImportOrderIndex;
|
||||
},
|
||||
);
|
||||
|
||||
// add the sorted imports to the insertion point keep the before and after content
|
||||
// add the type imports after the sorted imports
|
||||
const joinedModuleImports = sortedImports
|
||||
.map(([, importLine]) => importLine)
|
||||
.join("");
|
||||
const joinedTypeImports = [
|
||||
...typeImports.map((importMatch) => importMatch.statement),
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
newContent =
|
||||
newContent.substring(0, insertionPoint) +
|
||||
joinedModuleImports +
|
||||
joinedTypeImports +
|
||||
newContent.substring(insertionPoint);
|
||||
|
||||
return newContent;
|
||||
};
|
||||
23
packages/cli/src/utils/swizzle/index.ts
Normal file
23
packages/cli/src/utils/swizzle/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import path from "path";
|
||||
import type { RefineConfig } from "@definitions";
|
||||
import { provideCliHelpers } from "./provideCliHelpers";
|
||||
|
||||
export const getRefineConfig = async (
|
||||
packagePath: string,
|
||||
isAbsolute?: boolean,
|
||||
) => {
|
||||
try {
|
||||
provideCliHelpers(packagePath, isAbsolute);
|
||||
|
||||
const config = require(
|
||||
path.join(
|
||||
isAbsolute ? packagePath : path.join(process.cwd(), packagePath),
|
||||
"refine.config.js",
|
||||
),
|
||||
) as RefineConfig;
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
25
packages/cli/src/utils/swizzle/parseSwizzleBlocks.test.ts
Normal file
25
packages/cli/src/utils/swizzle/parseSwizzleBlocks.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { parseSwizzleBlocks } from "./parseSwizzleBlocks";
|
||||
|
||||
describe("parseSwizzleBlocks", () => {
|
||||
it("should remove swizzle blocks", () => {
|
||||
const content = `
|
||||
1
|
||||
// swizzle-remove-start
|
||||
remove-this
|
||||
//swizzle-remove-end
|
||||
2
|
||||
/* swizzle-remove-start */
|
||||
remove-this-too
|
||||
/* swizzle-remove-end */
|
||||
`;
|
||||
|
||||
const result = parseSwizzleBlocks(content);
|
||||
|
||||
expect(result).not.toContain("remove-this");
|
||||
expect(result).not.toContain("remove-this-too");
|
||||
expect(result).toContain("1");
|
||||
expect(result).toContain("2");
|
||||
expect(result).not.toContain("swizzle-remove-start");
|
||||
expect(result).not.toContain("swizzle-remove-end");
|
||||
});
|
||||
});
|
||||
6
packages/cli/src/utils/swizzle/parseSwizzleBlocks.ts
Normal file
6
packages/cli/src/utils/swizzle/parseSwizzleBlocks.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const parseSwizzleBlocks = (content: string) => {
|
||||
const regex =
|
||||
/(\/\/|\/\*)(\s?)swizzle-remove-start([\s\S]*?)(\/\/|\/\*)(\s?)swizzle-remove-end(\s*)(\*\/)?/g;
|
||||
|
||||
return content.replace(regex, "");
|
||||
};
|
||||
14
packages/cli/src/utils/swizzle/prettierFormat.ts
Normal file
14
packages/cli/src/utils/swizzle/prettierFormat.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { format, resolveConfig } from "prettier";
|
||||
|
||||
export const prettierFormat = async (code: string) => {
|
||||
try {
|
||||
const prettierConfig = await resolveConfig(process.cwd());
|
||||
|
||||
return format(code, {
|
||||
...(prettierConfig ?? {}),
|
||||
parser: "typescript",
|
||||
});
|
||||
} catch (err) {
|
||||
return code;
|
||||
}
|
||||
};
|
||||
32
packages/cli/src/utils/swizzle/provideCliHelpers.ts
Normal file
32
packages/cli/src/utils/swizzle/provideCliHelpers.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import path from "path";
|
||||
import * as RefineCLI from "../../index";
|
||||
import { getFileContent } from "./getFileContent";
|
||||
|
||||
const Module = require("module");
|
||||
const originalRequire = Module.prototype.require;
|
||||
|
||||
export const provideCliHelpers = (
|
||||
packagePath: string,
|
||||
isAbsolute?: boolean,
|
||||
) => {
|
||||
Module.prototype.require = function (...args: Parameters<NodeRequire>) {
|
||||
if ((args[0] as unknown as string) === "@refinedev/cli") {
|
||||
return {
|
||||
...RefineCLI,
|
||||
getFileContent: (filePath: string) => {
|
||||
return getFileContent.call(
|
||||
{
|
||||
absolutePackageDir: isAbsolute
|
||||
? packagePath
|
||||
: path.join(process.cwd(), packagePath),
|
||||
},
|
||||
filePath,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
//do your thing here
|
||||
return originalRequire.apply(this, args);
|
||||
};
|
||||
};
|
||||
46
packages/cli/src/utils/swizzle/renderCodeMarkdown.ts
Normal file
46
packages/cli/src/utils/swizzle/renderCodeMarkdown.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import chalk from "chalk";
|
||||
import cardinal from "cardinal";
|
||||
import boxen from "boxen";
|
||||
|
||||
const getCodeData = (content: string): { title?: string; code: string } => {
|
||||
const titleRegexp = /^(?:\/\/\s?title:\s?)(.*?)\n/g;
|
||||
|
||||
const [commentLine, titleMatch] = titleRegexp.exec(content) ?? [];
|
||||
|
||||
if (titleMatch) {
|
||||
const title = titleMatch.trim();
|
||||
const code = content.replace(commentLine || "", "");
|
||||
|
||||
return { title, code };
|
||||
}
|
||||
|
||||
return { code: content };
|
||||
};
|
||||
|
||||
export const renderCodeMarkdown = (content: string) => {
|
||||
const { title, code: rawCode } = getCodeData(content);
|
||||
|
||||
let highlighted = "";
|
||||
|
||||
// run cardinal on codeContent
|
||||
try {
|
||||
const code = cardinal.highlight(rawCode, {
|
||||
jsx: true,
|
||||
});
|
||||
highlighted = code;
|
||||
} catch (err) {
|
||||
highlighted = rawCode;
|
||||
}
|
||||
|
||||
// wrap to boxen
|
||||
const boxed = boxen(highlighted, {
|
||||
padding: 1,
|
||||
margin: 0,
|
||||
borderStyle: "round",
|
||||
borderColor: "gray",
|
||||
titleAlignment: "left",
|
||||
title: title ? chalk.bold(title) : undefined,
|
||||
});
|
||||
|
||||
return boxed;
|
||||
};
|
||||
29
packages/cli/src/utils/text/index.test.tsx
Normal file
29
packages/cli/src/utils/text/index.test.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { removeANSIColors, uppercaseFirstChar } from ".";
|
||||
|
||||
describe("uppercaseFirstChar", () => {
|
||||
it("should return the string with the first character capitalized", () => {
|
||||
const str = "hello world";
|
||||
const result = uppercaseFirstChar(str);
|
||||
expect(result).toEqual("Hello world");
|
||||
});
|
||||
|
||||
it("should return an empty string if the input is empty", () => {
|
||||
const str = "";
|
||||
const result = uppercaseFirstChar(str);
|
||||
expect(result).toEqual("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeANSIColors", () => {
|
||||
it("should remove ANSI color codes from the string", () => {
|
||||
const str = "\u001b[31mHello \u001b[32mworld\u001b[0m";
|
||||
const result = removeANSIColors(str);
|
||||
expect(result).toEqual("Hello world");
|
||||
});
|
||||
|
||||
it("should return the original string if it does not contain any ANSI color codes", () => {
|
||||
const str = "Hello world";
|
||||
const result = removeANSIColors(str);
|
||||
expect(result).toEqual(str);
|
||||
});
|
||||
});
|
||||
11
packages/cli/src/utils/text/index.ts
Normal file
11
packages/cli/src/utils/text/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const uppercaseFirstChar = (str: string): string => {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
export const removeANSIColors = (str: string): string => {
|
||||
return str.replace(
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: we want to remove invisible characters here.
|
||||
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
|
||||
"",
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user