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

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env node
import { server } from "./index";
server().catch(() => 0);

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' fill='none' viewBox='0 0 40 40'%3E%3Cpath fill='%231D1E30' fill-rule='evenodd' d='M23.9975 12.3333c0 2.2092-1.7909 4-4 4-2.2091 0-4-1.7908-4-4 0-2.2091 1.7909-4 4-4 2.2091 0 4 1.7909 4 4Z' clip-rule='evenodd'/%3E%3Cpath fill='%231D1E30' fill-rule='evenodd' d='M13.7975 12.2c0-3.4242 2.7758-6.2 6.2-6.2 3.4242 0 6.2 2.7758 6.2 6.2v15.6c0 3.4242-2.7758 6.2-6.2 6.2-3.4242 0-6.2-2.7758-6.2-6.2V12.2Zm6.2-5.2c-2.8719 0-5.2 2.3281-5.2 5.2v15.6c0 2.8719 2.3281 5.2 5.2 5.2s5.2-2.3281 5.2-5.2V12.2c0-2.8719-2.3281-5.2-5.2-5.2Z' clip-rule='evenodd'/%3E%3Cpath fill='%231D1E30' fill-rule='evenodd' d='M15.5279 1.0557a9.9998 9.9998 0 0 1 8.9442 0l10 5C37.86 7.7497 40 11.2123 40 15v10c0 3.7877-2.14 7.2504-5.5279 8.9443l-10 5a10 10 0 0 1-8.9442 0l-10-5C2.14 32.2504 0 28.7877 0 25V15c0-3.7877 2.14-7.2504 5.5279-8.9443l10-5Zm.4472.8945a8.9999 8.9999 0 0 1 8.0498 0l10 5C37.074 8.4747 39 11.591 39 15v10c0 3.4089-1.926 6.5253-4.9751 8.0498l-10 5a8.9997 8.9997 0 0 1-8.0498 0l-10-5C2.926 31.5253 1 28.4089 1 25V15c0-3.4089 1.926-6.5253 4.975-8.0498l10.0001-5Z' clip-rule='evenodd'/%3E%3Cpath fill='%231D1E30' fill-rule='evenodd' d='M15.9751 1.9502a8.9999 8.9999 0 0 1 8.0498 0l10 5C37.074 8.4747 39 11.591 39 15v10c0 3.4089-1.926 6.5253-4.9751 8.0498l-10 5a8.9997 8.9997 0 0 1-8.0498 0l-10-5C2.926 31.5253 1 28.4089 1 25V15c0-3.4089 1.926-6.5253 4.975-8.0498l10.0001-5ZM19.9975 6c-3.4242 0-6.2 2.7758-6.2 6.2v15.6c0 3.4242 2.7758 6.2 6.2 6.2 3.4242 0 6.2-2.7758 6.2-6.2V12.2c0-3.4242-2.7758-6.2-6.2-6.2Z' clip-rule='evenodd'/%3E%3Cpath fill='url(%23paint0_linear_504_1891)' fill-rule='evenodd' d='M23.9975 12.3333c0 2.2092-1.7909 4-4 4-2.2091 0-4-1.7908-4-4 0-2.2091 1.7909-4 4-4 2.2091 0 4 1.7909 4 4Z' clip-rule='evenodd'/%3E%3Cpath fill='url(%23paint1_linear_504_1891)' fill-rule='evenodd' d='M13.7975 12.2c0-3.4242 2.7758-6.2 6.2-6.2 3.4242 0 6.2 2.7758 6.2 6.2v15.6c0 3.4242-2.7758 6.2-6.2 6.2-3.4242 0-6.2-2.7758-6.2-6.2V12.2Zm6.2-5.2c-2.8719 0-5.2 2.3281-5.2 5.2v15.6c0 2.8719 2.3281 5.2 5.2 5.2s5.2-2.3281 5.2-5.2V12.2c0-2.8719-2.3281-5.2-5.2-5.2Z' clip-rule='evenodd'/%3E%3Cpath fill='url(%23paint2_linear_504_1891)' fill-rule='evenodd' d='M15.5279 1.0557a9.9998 9.9998 0 0 1 8.9442 0l10 5C37.86 7.7497 40 11.2123 40 15v10c0 3.7877-2.14 7.2504-5.5279 8.9443l-10 5a10 10 0 0 1-8.9442 0l-10-5C2.14 32.2504 0 28.7877 0 25V15c0-3.7877 2.14-7.2504 5.5279-8.9443l10-5Zm.4472.8945a8.9999 8.9999 0 0 1 8.0498 0l10 5C37.074 8.4747 39 11.591 39 15v10c0 3.4089-1.926 6.5253-4.9751 8.0498l-10 5a8.9997 8.9997 0 0 1-8.0498 0l-10-5C2.926 31.5253 1 28.4089 1 25V15c0-3.4089 1.926-6.5253 4.975-8.0498l10.0001-5Z' clip-rule='evenodd'/%3E%3Cpath fill='url(%23paint3_radial_504_1891)' fill-rule='evenodd' d='M15.9751 1.9502a8.9999 8.9999 0 0 1 8.0498 0l10 5C37.074 8.4747 39 11.591 39 15v10c0 3.4089-1.926 6.5253-4.9751 8.0498l-10 5a8.9997 8.9997 0 0 1-8.0498 0l-10-5C2.926 31.5253 1 28.4089 1 25V15c0-3.4089 1.926-6.5253 4.975-8.0498l10.0001-5ZM19.9975 6c-3.4242 0-6.2 2.7758-6.2 6.2v15.6c0 3.4242 2.7758 6.2 6.2 6.2 3.4242 0 6.2-2.7758 6.2-6.2V12.2c0-3.4242-2.7758-6.2-6.2-6.2Z' clip-rule='evenodd'/%3E%3Cdefs%3E%3ClinearGradient id='paint0_linear_504_1891' x1='20' x2='20' y1='8.5' y2='16.5' gradientUnits='userSpaceOnUse'%3E%3Cstop stop-color='%2347EBEB'/%3E%3Cstop offset='1' stop-color='%2347EBEB' stop-opacity='.5'/%3E%3C/linearGradient%3E%3ClinearGradient id='paint1_linear_504_1891' x1='20' x2='20' y1='6' y2='34' gradientUnits='userSpaceOnUse'%3E%3Cstop stop-color='%2347EBEB'/%3E%3Cstop offset='1' stop-color='%2347EBEB' stop-opacity='.5'/%3E%3C/linearGradient%3E%3ClinearGradient id='paint2_linear_504_1891' x1='20' x2='20' y1='0' y2='40' gradientUnits='userSpaceOnUse'%3E%3Cstop stop-color='%2347EBEB'/%3E%3Cstop offset='.5' stop-color='%2347EBEB' stop-opacity='.5'/%3E%3Cstop offset='1' stop-color='%2347EBEB' stop-opacity='.5'/%3E%3C/linearGradient%3E%3CradialGradient id='paint3_radial_504_1891' cx='0' cy='0' r='1' gradientTransform='matrix(0 40 -40 0 20 0)' gradientUnits='userSpaceOnUse'%3E%3Cstop stop-color='%2347EBEB' stop-opacity='0'/%3E%3Cstop offset='.5' stop-color='%2347EBEB' stop-opacity='.25'/%3E%3Cstop offset='1' stop-color='%2347EBEB' stop-opacity='.5'/%3E%3C/radialGradient%3E%3C/defs%3E%3C/svg%3E"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<!-- <link rel="manifest" href="/manifest.json" /> -->
<title>Refine Devtools</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm run dev` or `yarn dev`.
To create a production bundle, use `npm run build` or `yarn build`.
--></body>
</html>

View File

@@ -0,0 +1,10 @@
import { renderDevTools } from "@refinedev/devtools-ui";
import "@refinedev/devtools-ui/style.css";
const container = document.getElementById("root");
if (container) {
renderDevTools(container);
} else {
throw new Error("Could not find root element");
}

View File

@@ -0,0 +1,11 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
root: "src/client",
build: {
emptyOutDir: true,
outDir: "../../dist/client",
},
});

View File

@@ -0,0 +1,25 @@
import dotenv from "dotenv";
const refineEnv: Record<string, string> = {};
dotenv.config({ processEnv: refineEnv });
const RAW_ENV_REFINE_DEVTOOLS_PORT =
process.env.REFINE_DEVTOOLS_PORT || refineEnv.REFINE_DEVTOOLS_PORT;
export const DEFAULT_SERVER_PORT = 5001;
export const SERVER_PORT =
Number(RAW_ENV_REFINE_DEVTOOLS_PORT) || DEFAULT_SERVER_PORT;
export const AUTH_SERVER_URL = __DEVELOPMENT__
? "https://develop.auth.refine.dev"
: "https://auth.refine.dev";
export const REFINE_API_URL = __DEVELOPMENT__
? "https://develop.cloud.refine.dev"
: "https://cloud2.refine.dev";
export const AUTH_TRIGGER_API_PATH = "/api/login";
export const AUTH_CALLBACK_API_PATH = "/api/login-callback";
export const AUTH_CALLBACK_UI_PATH = "/after-login";
export const FEED_MD_URL =
"https://raw.githubusercontent.com/refinedev/refine/master/packages/devtools-server/FEED.md";

View File

@@ -0,0 +1,34 @@
import type {
DevtoolsEvent,
DevtoolsEventPayloads,
} from "@refinedev/devtools-shared";
import type { WebSocket } from "ws";
export type Activity =
DevtoolsEventPayloads[DevtoolsEvent.DEVTOOLS_ACTIVITY_UPDATE]["updatedActivities"][number];
export type Data = {
connectedApp: null | string;
clientWs: null | WebSocket;
devtoolsWsClients: WebSocket[];
appWsClients: WebSocket[];
activities: Activity[];
packages: string[];
};
const defaultData: Data = {
connectedApp: null,
clientWs: null,
// connections
devtoolsWsClients: [],
appWsClients: [],
// data
activities: [],
packages: [],
};
export const createDb = (): Data => {
return {
...defaultData,
};
};

View File

@@ -0,0 +1 @@
declare const __DEVELOPMENT__: boolean;

View File

@@ -0,0 +1,65 @@
import fetch from "node-fetch";
import matter from "gray-matter";
import { marked } from "marked";
import sanitizeHtml from "sanitize-html";
import type { Feed, FeedSection } from "@refinedev/devtools-shared";
import { FEED_MD_URL } from "src/constants";
const splitSections = (feed: string) => {
const sections = feed.split("---section");
return sections.slice(1).map((section) => `---section${section}`);
};
const contentToHtml = (content: string) => {
const renderer = new marked.Renderer();
renderer.link = function (...args) {
const out = marked.Renderer.prototype.link.apply(this, args);
return out.replace(/^<a/, '<a target="_blank" rel="noopener noreferrer"');
};
const html = marked(content, {
renderer,
});
return sanitizeHtml(html, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ["src"],
},
});
};
const parseSection = (section: string): FeedSection => {
const parsed = matter(section.replace("---section", "---"));
return {
...parsed.data,
content: contentToHtml(parsed.content),
} as FeedSection;
};
const fetchFeed = async () => {
try {
const response = await fetch(FEED_MD_URL);
return response.text();
} catch (_) {
return "";
}
};
export const getFeed = async (): Promise<Feed> => {
try {
const rawContent = await fetchFeed();
const rawSections = splitSections(rawContent);
const sections = rawSections.map(parseSection);
return sections;
} catch (e) {
return [];
}
};

View File

@@ -0,0 +1,165 @@
import express from "express";
import { DevtoolsEvent, receive, send } from "@refinedev/devtools-shared";
import { serveClient } from "./serve-client";
import { serveWs } from "./serve-ws";
import { reloadOnChange } from "./reload-on-change";
import { setupServer } from "./setup-server";
import { type Activity, createDb } from "./create-db";
import { serveApi } from "./serve-api";
import { serveProxy } from "./serve-proxy";
import { serveOpenInEditor } from "./serve-open-in-editor";
type Options = {
projectPath?: string;
onError?: () => void;
};
export const server = async ({
projectPath = process.cwd(),
onError = () => {
process.exit(1);
},
}: Options = {}) => {
return new Promise((_, reject) => {
const app = express();
const server = setupServer(app, () => {
reject();
onError();
});
const ws = serveWs(server, () => {
reject();
onError();
});
const db = createDb();
ws.on("connection", (client) => {
// Initialize development client
receive(client as any, DevtoolsEvent.DEVTOOLS_INIT, (data) => {
if (db.connectedApp) {
// send client the devtools client url if already connected
send(client as any, DevtoolsEvent.DEVTOOLS_ALREADY_CONNECTED, {
url: db.connectedApp,
});
} else {
db.connectedApp = data.url;
db.clientWs = client;
ws.clients.forEach((c) => {
send(c as any, DevtoolsEvent.DEVTOOLS_CONNECTED_APP, {
url: db.connectedApp,
});
});
}
});
receive(client as any, DevtoolsEvent.ACTIVITY, (data) => {
// match by identifier, if identifier is same, update data instead of pushing
const index = db.activities.findIndex(
(activity) => activity.identifier === data.identifier,
);
const record: Activity = {
...data,
createdAt: Date.now(),
updatedAt: Date.now(),
};
if (index > -1) {
record.createdAt = db.activities[index].createdAt;
db.activities[index] = record;
} else {
db.activities.push(record);
}
ws.clients.forEach((c) => {
send(c as any, DevtoolsEvent.DEVTOOLS_ACTIVITY_UPDATE, {
updatedActivities: [record],
});
});
});
receive(
client as any,
DevtoolsEvent.DEVTOOLS_HIGHLIGHT_IN_MONITOR,
({ name }) => {
ws.clients.forEach((c) => {
send(c as any, DevtoolsEvent.DEVTOOLS_HIGHLIGHT_IN_MONITOR_ACTION, {
name,
});
});
},
);
receive(
client as any,
DevtoolsEvent.DEVTOOLS_INVALIDATE_QUERY,
({ queryKey }) => {
ws.clients.forEach((c) => {
send(c as any, DevtoolsEvent.DEVTOOLS_INVALIDATE_QUERY_ACTION, {
queryKey,
});
});
},
);
receive(client as any, DevtoolsEvent.DEVTOOLS_LOGIN_SUCCESS, () => {
ws.clients.forEach((c) => {
send(c as any, DevtoolsEvent.DEVTOOLS_RELOAD_AFTER_LOGIN, {});
});
});
receive(
client as any,
DevtoolsEvent.DEVTOOLS_LOGIN_FAILURE,
({ error, code }) => {
ws.clients.forEach((c) => {
send(c as any, DevtoolsEvent.DEVTOOLS_DISPLAY_LOGIN_FAILURE, {
error,
code,
});
});
},
);
// close connected app if client disconnects
client.on("close", (_, reason) => {
if (__DEVELOPMENT__) {
console.log("Client disconnected", ws.clients.size);
}
if (db.clientWs) {
if (!ws.clients.has(db.clientWs)) {
db.clientWs = null;
db.connectedApp = null;
db.activities = [];
ws.clients.forEach((c) => {
send(c as any, DevtoolsEvent.DEVTOOLS_DISCONNECTED_APP, {
url: db.connectedApp,
});
});
}
}
});
if (__DEVELOPMENT__) {
console.log("Client connected", ws.clients.size);
}
});
reloadOnChange(ws);
serveClient(app);
serveApi(app, db);
serveProxy(app);
serveOpenInEditor(app, projectPath);
process.on("SIGTERM", () => {
reject();
});
});
};

View File

@@ -0,0 +1,28 @@
import type { PackageType } from "@refinedev/devtools-shared";
import { getInstalledPackageData } from "./get-installed-package-data";
import { getPackagesFromPackageJSON } from "./get-packages-from-package-json";
import { getChangelog } from "./get-changelog";
import { getDocumentation } from "./get-documentation";
export const getAllPackages = async (projectPath?: string) => {
try {
const refinePackages = await getPackagesFromPackageJSON(projectPath);
const installedVersions = await Promise.all(
refinePackages.map(async (packageName) => {
const currentInfo = await getInstalledPackageData(packageName);
return {
name: packageName,
currentVersion: currentInfo?.version,
description: currentInfo?.description,
changelog: getChangelog(packageName),
documentation: getDocumentation(packageName),
} as PackageType;
}),
);
return installedVersions;
} catch (error) {
return [];
}
};

View File

@@ -0,0 +1,514 @@
import type { AvailablePackageType } from "@refinedev/devtools-shared";
import dedent from "dedent";
import { getPackagesFromPackageJSON } from "./get-packages-from-package-json";
export const AVAILABLE_PACKAGES: AvailablePackageType[] = [
{
name: "@refinedev/ably",
description: "Ably integration for Refine",
install: "npm install @refinedev/ably",
usage: dedent(
`
import { liveProvider, Ably } from "@refinedev/ably";
export const ablyClient = new Ably.Realtime("YOUR_API_TOKEN");
const App = () => {
return (
<Refine
liveProvider={liveProvider(ablyClient)}
/* ... */
>
{/* ... */}
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/airtable",
description: "Airtable integration for Refine",
install: "npm install @refinedev/airtable",
usage: dedent(
`
import dataProvider from "@refinedev/airtable";
const App = () => {
return (
<Refine
dataProvider={dataProvider("API_KEY", "BASE_ID")}
/* ... */
>
{/* ... */}
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/antd",
description: "Ant Design integration for Refine",
install: "npm install @refinedev/antd antd",
usage: dedent(
`
import { ThemedLayoutV2 } from "@refinedev/antd";
import "@refinedev/antd/dist/reset.css";
const App = () => {
return (
<Refine
/* ... */
>
<ThemedLayoutV2>
{/* ... */}
</ThemedLayoutV2>
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/appwrite",
description: "Appwrite integration for Refine",
install: "npm install @refinedev/appwrite",
usage: dedent(
`
import { dataProvider, liveProvider, Account, Appwrite, Storage } from "@refinedev/appwrite";
const appwriteClient = new Appwrite();
appwriteClient.setEndpoint("API_URL").setProject("PROJECT_ID");
const App = () => {
return (
<Refine
dataProvider={dataProvider(appwriteClient, { databaseId: "default" })}
liveProvider={liveProvider(appwriteClient, { databaseId: "default" })}
/* ... */
>
<ThemedLayout>
{/* ... */}
</ThemedLayout>
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/chakra-ui",
description: "Chakra UI integration for Refine",
install:
"npm install @refinedev/chakra-ui @chakra-ui/react @emotion/react @emotion/styled framer-motion @tabler/icons-react",
usage: dedent(
`
import { ThemedLayoutV2 } from "@refinedev/chakra-ui";
import { ChakraProvider } from "@chakra-ui/react";
const App = () => {
return (
<ChakraProvider>
<Refine
/* ... */
>
<ThemedLayoutV2>
{/* ... */}
</ThemedLayoutV2>
</Refine>
</ChakraProvider>
);
};
`.trim(),
),
},
{
name: "@refinedev/graphql",
description: "GraphQL integration for Refine",
install: "npm install @refinedev/graphql",
usage: dedent(
`
import dataProvider, { GraphQLClient } from "@refinedev/graphql";
const client = new GraphQLClient("YOUR_API_URL");
const App = () => {
return (
<Refine
dataProvider={dataProvider(client)}
/* ... */
>
{/* ... */}
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/hasura",
description: "GraphQL integration for Refine",
install: "npm install @refinedev/hasura",
usage: dedent(
`
import dataProvider, { GraphQLClient } from "@refinedev/hasura";
const client = new GraphQLClient("HASURA_API_URL", {
headers: {
"x-hasura-role": "public",
},
});
const App = () => {
return (
<Refine
dataProvider={dataProvider(client)}
/* ... */
>
{/* ... */}
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/inferencer",
description: "Auto generate views based on your APIs with Refine",
install: "npm install @refinedev/inferencer",
usage: dedent(
`
import { AntdInferencer } from "@refinedev/inferencer/antd";
const App = () => {
return (
<Refine
/* ... */
>
<AntdInferencer action="list" resource="posts" />
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/kbar",
description: "Command palette integration with kbar for Refine",
install: "npm install @refinedev/kbar",
usage: dedent(
`
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
const App = () => {
return (
<RefineKbarProvider>
<Refine
/* ... */
>
<RefineKbar />
</Refine>
</RefineKbarProvider>
);
};
`.trim(),
),
},
{
name: "@refinedev/mantine",
description: "Mantine UI integration for Refine",
install:
"npm install @refinedev/mantine @refinedev/react-table @mantine/core @mantine/hooks @mantine/form @mantine/notifications @emotion/react @tabler/icons-react",
usage: dedent(
`
import { ThemedLayoutV2 } from "@refinedev/mantine";
import { MantineProvider } from "@mantine/core";
const App = () => {
return (
<MantineProvider>
<Refine
/* ... */
>
<ThemedLayoutV2>
{/* ... */}
</ThemedLayoutV2>
</Refine>
</MantineProvider>
);
};
`.trim(),
),
},
{
name: "@refinedev/medusa",
description: "Medusa store integration for Refine",
install: "npm install @refinedev/medusa",
usage: dedent(
`
import dataProvider, { authProvider } from "@refinedev/medusa";
const App = () => {
return (
<Refine
dataProvider={dataProvider("API_URL")}
authProvider={authProvider("API_URL")}
/* ... */
>
{/* ... */}
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/mui",
description: "Material UI integration for Refine",
install:
"npm install @refinedev/mui @refinedev/react-hook-form @mui/material @mui/lab @mui/x-data-grid @emotion/react @emotion/styled react-hook-form",
usage: dedent(
`
import { ThemedLayoutV2 } from "@refinedev/mui";
import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import { ThemeProvider } from "@mui/material/styles";
const App = () => {
return (
<ThemeProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<Refine
/* ... */
>
<ThemedLayoutV2>
{/* ... */}
</ThemedLayoutV2>
</Refine>
</ThemeProvider>
);
};
`.trim(),
),
},
{
name: "@refinedev/nestjs-query",
description: "NestJS Query data provider integration for Refine",
install: "npm install @refinedev/nestjs-query graphql-ws",
usage: dedent(
`
import dataProvider, {
GraphQLClient,
liveProvider,
} from "@refinedev/nestjs-query";
import { createClient } from "graphql-ws";
const App = () => {
return (
<Refine
dataProvider={dataProvider(new GraphQLClient( "API_URL" ))}
liveProvider={liveProvider(createClient({ url: "WS_URL" }))}
/* ... */
>
{/* ... */}
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/nestjsx-crud",
description: "NestJSX CRUD data provider integration for Refine",
install: "npm install @refinedev/nestjsx-crud",
usage: dedent(
`
import dataProvider from "@refinedev/nestjsx-crud";
const App = () => {
return (
<Refine
dataProvider={dataProvider("API_URL")}
/* ... */
>
{/* ... */}
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/react-hook-form",
description: "React Hook Form integration for Refine",
install: "npm install @refinedev/react-hook-form react-hook-form",
usage: dedent(
`
import { useForm } from "@refinedev/react-hook-form";
const EditPost = () => {
const {
register,
handleSubmit,
formState,
refineCore,
} = useForm({
refineCoreProps: {
resource: "posts",
id: "1",
},
});
return /* ... */
};
`.trim(),
),
},
{
name: "@refinedev/react-table",
description: "Tanstack React Table integration for Refine",
install: "npm install @refinedev/react-table @tanstack/react-table",
usage: dedent(
`
import { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
const EditPost = () => {
const columns = React.useMemo<ColumnDef<IPost>[]>(
() => [
{
id: "id",
header: "ID",
accessorKey: "id",
},
{
id: "title",
header: "Title",
accessorKey: "title",
meta: {
filterOperator: "contains",
},
},
], []);
const tableInstance = useTable({
columns,
refineCoreProps: {
resource: "posts",
}
});
return /* ... */
};
`.trim(),
),
},
{
name: "@refinedev/simple-rest",
description: "Data provider integration for REST APIs with Refine",
install: "npm install @refinedev/simple-rest",
usage: dedent(
`
import dataProvider from "@refinedev/simple-rest";
const App = () => {
return (
<Refine
dataProvider={dataProvider("API_URL")}
/* ... */
>
{/* ... */}
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/supabase",
description:
"Data provider and live provider integrations for Supabase with Refine",
install: "npm install @refinedev/supabase",
usage: dedent(
`
import { dataProvider, liveProvider, createClient } from "@refinedev/supabase";
const supabaseClient = createClient("SUPABASE_URL", "SUPABASE_KEY");
const App = () => {
return (
<Refine
dataProvider={dataProvider(supabaseClient)}
liveProvider={liveProvider(supabaseClient)}
/* ... */
>
{/* ... */}
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/strapi",
description: "Strapi integration of Refine",
install: "npm install @refinedev/strapi axios",
usage: dedent(
`
import { DataProvider, AuthHelper } from "@refinedev/strapi";
const axiosInstance = axios.create();
const strapiAuthHelper = AuthHelper("API_URL");
const App = () => {
return (
<Refine
dataProvider={DataProvider("API_URL", axiosInstance)}
/* ... */
>
{/* ... */}
</Refine>
);
};
`.trim(),
),
},
{
name: "@refinedev/strapi-v4",
description: "StrapiV4 integration of Refine",
install: "npm install @refinedev/strapi-v4 axios",
usage: dedent(
`
import { DataProvider, AuthHelper } from "@refinedev/strapi-v4";
const axiosInstance = axios.create();
const strapiAuthHelper = AuthHelper("API_URL");
const App = () => {
return (
<Refine
dataProvider={DataProvider("API_URL", axiosInstance)}
/* ... */
>
{/* ... */}
</Refine>
);
};
`.trim(),
),
},
];
export const getAvailablePackages = async (projectPath?: string) => {
const installedRefinePackages = await getPackagesFromPackageJSON(projectPath);
return AVAILABLE_PACKAGES.filter(
(p) => !installedRefinePackages.includes(p.name),
);
};

View File

@@ -0,0 +1,3 @@
export const getChangelog = (packageName: string) => {
return packageName.replace("@refinedev/", "https://c.refine.dev/");
};

View File

@@ -0,0 +1,3 @@
export const getDocumentation = (packageName: string) => {
return "https://refine.dev/docs/api-reference/general-concepts/";
};

View File

@@ -0,0 +1,24 @@
import fs from "fs";
import { getInstalledPackageJSONPath } from "./get-installed-package-json-path";
export const getInstalledPackageData = async (packageName: string) => {
try {
const packagePath = await getInstalledPackageJSONPath(packageName);
if (!packagePath) {
return null;
}
const parsed = JSON.parse(
fs.readFileSync(packagePath, { encoding: "utf-8" }),
);
return {
name: parsed.name,
version: parsed.version,
description: parsed.description,
};
} catch (e) {
return null;
}
};

View File

@@ -0,0 +1,35 @@
import path from "path";
import globby from "globby";
export const getInstalledPackageJSONPath = async (packageName: string) => {
try {
const filesFromGlobbySearch = await globby(
`node_modules/${packageName}/package.json`,
{
onlyFiles: true,
},
);
let fileFromModule: string | null = null;
try {
const pkgJsonPath = require.resolve(
path.join(packageName, "package.json"),
);
if (pkgJsonPath) {
fileFromModule = pkgJsonPath;
}
} catch (err) {
//
}
return (
[
...filesFromGlobbySearch,
...(fileFromModule ? [fileFromModule] : []),
][0] ?? null
);
} catch (err) {
return null;
}
};

View File

@@ -0,0 +1,20 @@
import execa from "execa";
export const getLatestPackageData = async (
packageName: string,
): Promise<{ name: string; version?: string }> => {
try {
const { stdout } = await execa("npm", [
"view",
packageName,
"name",
"version",
"--json",
]);
const parsed = JSON.parse(stdout);
return parsed;
} catch (e) {
return { name: packageName };
}
};

View File

@@ -0,0 +1,16 @@
import path from "path";
import { readJSON } from "fs-extra";
export const getPackagesFromPackageJSON = async (
projectPath: string = process.cwd(),
) => {
const packageJson = await readJSON(path.join(projectPath, "package.json"), {
encoding: "utf-8",
});
const refinePackages = Object.keys(packageJson.dependencies).filter(
(packageName) => packageName.startsWith("@refinedev/"),
);
return refinePackages;
};

View File

@@ -0,0 +1,21 @@
import { detect } from "package-manager-detector";
import execa from "execa";
export const updatePackage = async (
packages: string[],
projectPath: string = process.cwd(),
) => {
try {
const detected = await detect({ cwd: projectPath });
const [pm] = (detected?.agent || "npm").split("@");
const { failed } = await execa(pm, [
"install",
...packages.map((p) => `${p}@latest`),
]);
return !failed;
} catch (error) {
return false;
}
};

View File

@@ -0,0 +1,22 @@
import { readJSON } from "fs-extra";
import path from "path";
export const getProjectIdFromPackageJson = async (
projectPath = process.cwd(),
) => {
try {
const packageJson = await readJSON(path.join(projectPath, "package.json"), {
encoding: "utf-8",
});
const projectId = packageJson?.refine?.projectId as string;
if (projectId) {
return projectId;
}
return false;
} catch (e) {
return null;
}
};

View File

@@ -0,0 +1,15 @@
import execa from "execa";
export const setProjectIdToPackageJson = async (
projectId: string,
projectPath = process.cwd(),
) => {
try {
execa.sync("npm", ["pkg", "set", `refine.projectId=${projectId}`], {
cwd: projectPath,
});
return true;
} catch (e) {
return null;
}
};

View File

@@ -0,0 +1,40 @@
import execa from "execa";
import path from "path";
export const setProjectIdToRefineComponent = async (
projectId: string,
projectPath = process.cwd(),
) => {
try {
const jscodeshiftExecutable = require.resolve(".bin/jscodeshift");
const execution = execa.sync(
jscodeshiftExecutable,
[
"./",
"--extensions=ts,tsx,js,jsx",
"--parser=tsx",
`--transform=${path.resolve(
path.join(__dirname, "..", "src", "project-id", "transform.ts"),
)}`,
"--ignore-pattern=**/.cache/**",
"--ignore-pattern=**/node_modules/**",
"--ignore-pattern=**/build/**",
"--ignore-pattern=**/dist/**",
"--ignore-pattern=**/.next/**",
`--__projectId=${projectId}`,
],
{
cwd: projectPath,
timeout: 1000 * 10,
},
);
if (execution.stderr) {
console.error(execution.stderr);
}
} catch (error) {
console.error(error);
}
return;
};

View File

@@ -0,0 +1,88 @@
import type { namedTypes } from "ast-types";
import type {
API,
ASTPath,
Collection,
FileInfo,
JSCodeshift,
Options,
} from "jscodeshift";
export const parser = "tsx";
const transformRefineOptions = (
j: JSCodeshift,
root: Collection<any>,
projectId: string,
) => {
const refineElements: Array<ASTPath<namedTypes.JSXElement>> = [];
root.findJSXElements("Refine").forEach((path) => {
refineElements.push(path);
});
for (const path of refineElements) {
const props = path.node.openingElement.attributes;
const optionsProp: any = props?.find(
(attribute) =>
attribute.type === "JSXAttribute" && attribute.name.name === "options",
);
if (!optionsProp) {
path.node.openingElement.attributes?.push(
j.jsxAttribute(
j.jsxIdentifier("options"),
j.jsxExpressionContainer(
j.objectExpression([
j.objectProperty(
j.identifier("projectId"),
j.stringLiteral(projectId),
),
]),
),
),
);
break;
}
// for options={optionsProp}
if (!optionsProp?.value.expression.properties) {
break;
}
// for options has already projectId
const hasProjectId = optionsProp?.value.expression.properties.find(
(p: any) => {
if (p.type === "ObjectProperty") {
return p.key.name === "projectId";
}
return false;
},
);
if (hasProjectId) break;
optionsProp?.value.expression.properties.push(
j.objectProperty(j.identifier("projectId"), j.stringLiteral(projectId)),
);
break;
}
return root;
};
export default function transformer(
file: FileInfo,
api: API,
options: Options,
) {
const j = api.jscodeshift;
const source = j(file.source);
transformRefineOptions(j, source, options.__projectId);
return source.toSource();
}

View File

@@ -0,0 +1,18 @@
import { setProjectIdToPackageJson } from "./set-project-id-to-package-json";
import { setProjectIdToRefineComponent } from "./set-project-id-to-refine-component";
export const updateProjectId = async (
projectId: string,
projectPath = process.cwd(),
) => {
try {
await Promise.all([
setProjectIdToPackageJson(projectId, projectPath),
setProjectIdToRefineComponent(projectId, projectPath),
]);
return true;
} catch (_) {
return false;
}
};

View File

@@ -0,0 +1,32 @@
import fs from "fs";
import path from "path";
import debounce from "lodash/debounce";
import { DevtoolsEvent, send } from "@refinedev/devtools-shared";
import type { Server } from "ws";
import { OPEN } from "ws";
export const reloadOnChange = __DEVELOPMENT__
? (ws: Server) => {
const reloadEmitter = debounce(() => {
setTimeout(() => {
ws.clients.forEach((client) => {
if (client.readyState === OPEN) {
console.log("Reloading connected client...");
send(client as any, DevtoolsEvent.RELOAD, {});
}
});
}, 800);
}, 1000);
const watcher = fs.watch(
path.resolve(__dirname, "client"),
{ recursive: true },
reloadEmitter,
);
process.on("SIGTERM", () => {
watcher.close();
});
}
: () => 0;

View File

@@ -0,0 +1,174 @@
import type { Express } from "express";
import { json } from "express";
import uniq from "lodash/uniq";
import type {
AvailablePackageType,
Feed,
PackageType,
} from "@refinedev/devtools-shared";
import type { Data } from "./create-db";
import { getFeed } from "./feed/get-feed";
import { getAllPackages } from "./packages/get-all-packages";
import { getAvailablePackages } from "./packages/get-available-packages";
import { updatePackage } from "./packages/update-package";
import { getLatestPackageData } from "./packages/get-latest-package-data";
import { getProjectIdFromPackageJson } from "./project-id/get-project-id-from-package-json";
import { updateProjectId } from "./project-id/update-project-id";
export const serveApi = (app: Express, db: Data) => {
app.use("/api", json());
app.get("/api/connected-app", (_, res) => {
res.json({ url: db.connectedApp });
});
app.get("/api/activities", (req, res) => {
const { offset = 0, limit = db.activities.length } = req.query;
res.setHeader("x-total-count", db.activities.length);
res.json({
data: db.activities.slice(Number(offset), Number(limit)),
});
});
app.get("/api/activities/reset", (_, res) => {
db.activities = [];
res.json({ success: true });
});
app.get("/api/unique-trace-items", (req, res) => {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept",
);
const traceItems = db.activities.flatMap(
(activity) =>
activity.trace?.map((t) => t.function).filter(Boolean) ?? [],
) as string[];
const uniqueTraceItems = uniq(traceItems);
res.setHeader("x-total-count", uniqueTraceItems.length);
res.json({ data: uniqueTraceItems });
});
let cachedInstalledPackages: PackageType[] | null = null;
app.get("/api/installed-packages", async (req, res) => {
const { force } = req.query ?? {};
if (!cachedInstalledPackages || force) {
cachedInstalledPackages = await getAllPackages();
}
res.header("x-total-count", `${cachedInstalledPackages.length}`);
res.json({ data: cachedInstalledPackages });
});
let cachedAvailablePackages: AvailablePackageType[] | null = null;
app.get("/api/available-packages", async (_, res) => {
if (!cachedAvailablePackages) {
cachedAvailablePackages = await getAvailablePackages();
}
res.header("x-total-count", `${cachedAvailablePackages.length}`);
res.json({ data: cachedAvailablePackages });
});
const cachedLatestPackages = new Map<string, any>();
app.get("/api/packages/:packageName/latest", async (req, res) => {
const { packageName } = req.params ?? {};
if (!packageName) {
res.status(400).json({ error: "Package name is required" });
return;
}
if (!cachedLatestPackages.has(packageName)) {
const latest = await getLatestPackageData(packageName);
cachedLatestPackages.set(packageName, latest);
}
return res.json({ data: cachedLatestPackages.get(packageName) });
});
app.post("/api/packages/install", async (req, res) => {
const { packages } = req.body ?? {};
if (packages?.length === 0) {
res.status(400).json({ error: "Package name is required" });
return;
}
const success = await updatePackage(packages as string[]);
if (success) {
cachedInstalledPackages = null;
cachedAvailablePackages = null;
res.status(200).json({ success: true });
} else {
res.status(400).json({
success: false,
error: "Failed to update package",
});
}
});
let cachedFeed: Feed | null = null;
app.get("/api/feed", async (req, res) => {
if (!cachedFeed) {
cachedFeed = await getFeed();
}
res.header("x-total-count", `${cachedFeed.length}`);
res.json({ data: cachedFeed });
});
app.get("/api/project-id/status", async (_, res) => {
const CODES = {
OK: 0,
NOT_FOUND: 1,
ERROR: 2,
};
const projectId = await getProjectIdFromPackageJson();
if (projectId) {
res.status(200).json({ projectId, status: CODES.OK });
return;
}
if (projectId === false) {
res.status(200).json({ projectId: null, status: CODES.NOT_FOUND });
return;
}
res.status(200).json({ projectId: null, status: CODES.ERROR });
return;
});
app.post("/api/project-id/update", async (req, res) => {
const { projectId } = req.body ?? {};
if (!projectId) {
res.status(400).json({ error: "Project ID is required" });
return;
}
const success = await updateProjectId(projectId);
if (success) {
res.status(200).json({ success: true });
return;
}
res.status(500).json({
success: false,
error: "Failed to update project ID",
});
return;
});
};

View File

@@ -0,0 +1,18 @@
import express from "express";
import path from "path";
import type { Express } from "express";
export const serveClient = (app: Express) => {
app.use(express.static(path.join(__dirname, "client")));
app.use((req, res, next) => {
if (req.path.startsWith("/api")) {
return next();
}
if (req.path.startsWith("/open-in-editor")) {
return next();
}
res.status(200).sendFile(path.join(`${__dirname}/client/index.html`));
});
};

View File

@@ -0,0 +1,16 @@
import type { Express } from "express";
import path from "path";
export const serveOpenInEditor = (app: Express, basePath: string) => {
app.get("/open-in-editor/*", (req, res) => {
const { line, column } = req.query;
const filePath = req.path.replace("/open-in-editor", "");
const vscodeUrl = `vscode://file/${path.join(basePath, filePath)}?${
line ? `line=${line}` : ""
}${column ? `&column=${column}` : ""}`;
res.redirect(vscodeUrl);
});
};

View File

@@ -0,0 +1,151 @@
import path from "path";
import { readJSON, writeJSON } from "fs-extra";
import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware";
import {
REFINE_API_URL,
AUTH_SERVER_URL,
AUTH_CALLBACK_API_PATH,
AUTH_CALLBACK_UI_PATH,
AUTH_TRIGGER_API_PATH,
} from "./constants";
import { getProjectIdFromPackageJson } from "./project-id/get-project-id-from-package-json";
import type { Express, RequestHandler } from "express";
const persistPath = path.join(__dirname, "..", ".persist.json");
const saveAuth = async (token?: string, jwt?: string) => {
try {
await writeJSON(persistPath, { token, jwt });
} catch (error) {
//
}
};
const loadAuth = async () => {
try {
return (await readJSON(persistPath)) as { token?: string; jwt?: string };
} catch (error) {
//
}
return {};
};
export const serveProxy = async (app: Express) => {
let { token, jwt } = await loadAuth();
const authProxy = createProxyMiddleware({
target: `${AUTH_SERVER_URL}/api/.auth`,
secure: false,
changeOrigin: true,
logger: __DEVELOPMENT__ ? console : undefined,
on: {
proxyReq: fixRequestBody,
proxyRes: (_proxyRes, req) => {
if (req.url?.includes("self-service/logout/api")) {
token = undefined;
jwt = undefined;
saveAuth();
}
},
},
});
const refineProxy = createProxyMiddleware({
target: `${REFINE_API_URL}/.refine`,
secure: false,
changeOrigin: true,
logger: __DEVELOPMENT__ ? console : undefined,
on: {
proxyReq: fixRequestBody,
},
});
let currentProjectId: string | null | false = null;
const projectIdAppender: RequestHandler = async (req, _res, next) => {
if (!currentProjectId) {
currentProjectId = await getProjectIdFromPackageJson();
}
if (currentProjectId) {
req.headers["x-project-id"] = currentProjectId;
}
next();
};
const appendAuth: RequestHandler = async (req, _res, next) => {
if (token) {
req.headers["X-Session-Token"] = token;
}
if (req.url?.includes("self-service/logout/api")) {
req.body = {
session_token: token,
};
req.headers["Content-Length"] = Buffer.byteLength(
JSON.stringify(req.body),
).toString();
}
next();
};
const appendJwt: RequestHandler = async (req, _res, next) => {
if (jwt) {
req.headers["Authorization"] = `Bearer ${jwt}`;
delete req.headers["cookie"];
}
next();
};
const loginCallback: RequestHandler = async (req, res, _next) => {
const query = req.query;
if (query.token && query.jwt) {
token = query.token as string;
jwt = query.jwt as string;
await saveAuth(query.token as string, query.jwt as string);
}
const errorParams = new URLSearchParams();
if (query.error) {
errorParams.set("error", query.error as string);
}
if (query.code) {
errorParams.set("code", query.code as string);
}
res.redirect(`${AUTH_CALLBACK_UI_PATH}?${errorParams.toString()}`);
};
const loginTrigger: RequestHandler = async (req, res, _next) => {
const query = req.query;
const protocol = req.secure ? "https" : "http";
const host = req.headers.host;
if (!host) {
res.redirect(`${AUTH_CALLBACK_API_PATH}?error=Missing%20Host`);
return;
}
const callbackUrl = `${protocol}://${host}${AUTH_CALLBACK_API_PATH}`;
const params = new URLSearchParams({
provider: query.provider as string,
returnUrl: encodeURIComponent(callbackUrl),
});
res.redirect(`${AUTH_SERVER_URL}/login?${params.toString()}`);
};
app.use(AUTH_TRIGGER_API_PATH, loginTrigger);
app.use(AUTH_CALLBACK_API_PATH, loginCallback);
app.use("/api/.auth", appendAuth, authProxy);
app.use("/api/.refine", projectIdAppender, appendJwt, refineProxy);
};

View File

@@ -0,0 +1,46 @@
import WebSocket from "ws";
import { SERVER_PORT } from "./constants";
import { bold, cyanBright } from "chalk";
import type http from "http";
export const serveWs = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
onError: () => void,
) => {
const ws = new WebSocket.Server({ server }).on("error", (error: any) => {
if (error?.code === "EADDRINUSE") {
console.error(
`\n${cyanBright.bold("\u2717 ")}${bold(
"Refine Devtools server",
)} (websocket) failed to start. Port ${SERVER_PORT} is already in use.\n`,
);
} else {
console.error(
`\n${cyanBright.bold("\u2717 ")}${bold("error from refine devtools")}`,
error,
);
}
ws.close(() => {
if (__DEVELOPMENT__) {
console.log("Process terminated");
}
});
onError();
});
ws.on("connection", (client) => {
client.on("close", () => {
client.terminate();
});
});
process.on("SIGTERM", () => {
ws.close(() => {
if (__DEVELOPMENT__) {
console.log("Process terminated");
}
});
});
return ws;
};

View File

@@ -0,0 +1,62 @@
import type { Express } from "express";
import { SERVER_PORT } from "./constants";
import { bold, cyanBright } from "chalk";
import http from "http";
export const setupServer = (app: Express, onError: () => void) => {
const server = http.createServer(app);
server
.on("error", (error: any) => {
if (error?.code === "EADDRINUSE") {
console.error(
`\n${cyanBright.bold("\u2717 ")}${bold(
"Refine Devtools server",
)} (http) failed to start. Port ${SERVER_PORT} is already in use.\n`,
);
console.info(
`${cyanBright.bold(
"\u2139 ",
)}You can change the port by setting the ${bold(
"REFINE_DEVTOOLS_PORT",
)} environment variable.`,
);
} else {
console.error(
`\n${cyanBright.bold("\u2717 ")}${bold(
"error from Refine Devtools",
)}`,
error,
);
}
server.close(() => {
if (__DEVELOPMENT__) {
console.log("Process terminated");
}
});
onError();
})
.on("listening", () => {
console.log(
`\n${cyanBright.bold("\u2713 ")}${bold(
"Refine Devtools",
)} is running at port ${cyanBright.bold(SERVER_PORT)}\n`,
);
});
process.on("SIGTERM", () => {
server.close(() => {
if (__DEVELOPMENT__) {
console.log("Process terminated");
}
});
});
server.listen(SERVER_PORT, undefined, undefined, () => {
if (__DEVELOPMENT__) {
console.log(`Server started on PORT ${SERVER_PORT}`);
}
});
return server;
};