إنتقل إلى المحتوى الرئيسي

تطوير إطار عمل Remix في Cursor: دليل شامل

تطوير Remix في Cursor

Remix هو إطار عمل ويب كامل المكدس مبني على React Router يُركّز على التحسين التدريجي ومعايير الويب والعرض من جانب الخادم. قدّم Remix v2 المسارات المسطحة، ومعالجة أخطاء محسّنة، ونموذج تحميل بيانات مبسّط. يُعد Cursor، بأساسه المتوافق مع VS Code ومساعد الذكاء الاصطناعي المدمج، محرراً مثالياً لتطوير Remix -- فهو يفهم ملفات مسارات .tsx، ويوفر IntelliSense لأنواع إرجاع أدوات التحميل، ويمكنه إنشاء وحدات مسارات كاملة من أوامر لغة طبيعية.

يغطي هذا الدليل سير عمل تطوير Remix الكامل في Cursor: إعداد المشروع، اصطلاحات التوجيه، أدوات التحميل والإجراءات، المسارات المتداخلة، حدود الأخطاء، وإنشاء المسارات بمساعدة الذكاء الاصطناعي.

إعداد تطوير Remix في Cursor​

المتطلبات الأساسية​

يتطلب Remix v2 إصدار Node.js 18 أو أحدث (يُوصى بـ Node 20 LTS). تحقق من التثبيت:

node --version
npm --version
تلميح

الطرفية المدمجة في Cursor (Ctrl+`) تتزامن مع لوحة Explorer، لذا يمكنك إنشاء مشروع وتحرير الملفات المُنشأة فوراً دون مغادرة الكود.

تثبيت إضافة Remix​

يرث Cursor توافق إضافات VS Code. لتطوير Remix، ثبّت هذه الإضافات من عرض الإضافات (Ctrl+Shift+X):

  1. ES7 React/Redux/GraphQL/React-Native snippets -- مفيدة لهيكل مكونات React.
  2. Tailwind CSS IntelliSense -- إذا كنت تستخدم Tailwind للتنسيق.
  3. Prisma -- إذا كان تطبيق Remix يستخدم Prisma للبيانات.

دعم TypeScript المدمج في Cursor يتعامل مع استدلال أنواع Remix بشكل أصلي، بما في ذلك أنواع MetaFunction و LoaderFunction و ActionFunction.

warning

لا تثبّت إضافة تمييز صياغة "Remix" المُهمَلة. ملفات Remix هي ملفات .tsx/.ts قياسية يدعمها Cursor أصلياً. الإضافة المُهمَلة قد تتعارض مع خادم لغة TypeScript في Cursor.

إنشاء مشروع Remix​

استخدم أداة Remix الرسمية لإنشاء مشروع جديد:

npx create-remix@latest my-remix-app --template remix-run/default --no-install --no-git

cd my-remix-app
npm install

افتح المشروع في Cursor:

cursor .

هيكل المشروع الافتراضي:

my-remix-app/
├── app/
│ ├── components/
│ ├── routes/
│ │ └── _index.tsx
│ ├── entry.client.tsx
│ ├── entry.server.tsx
│ ├── root.tsx
│ ├── tailwind.css
│ └── entry.client.tsx
├── public/
├── vite.config.ts
├── tsconfig.json
└── package.json
تلميح

اطلب من Cursor إنشاء مشروع كامل: "Create a Remix v2 project with TypeScript, Tailwind CSS, and Prisma. Set up the vite config and a root layout with a navigation bar." سيقوم Cursor بإنشاء جميع ملفات التكوين وهيكل المسارات الأولي.

اصطلاحات التوجيه​

يستخدم Remix v2 التوجيه المسطح القائم على الملفات. ملفات المسارات في دليل app/routes/ ترتبط مباشرة بمسارات URL.

المسارات الأساسية​

الملفمسار URL
app/routes/_index.tsx/
app/routes/about.tsx/about
app/routes/blog.tsx/blog
app/routes/blog.$slug.tsx/blog/:slug

المسارات المتداخلة مع المسارات المسطحة​

تستخدم التخطيطات المتداخلة تدوين النقاط. ملف باسم app/routes/blog._index.tsx يُعرض داخل تخطيط app/routes/blog.tsx:

app/routes/
├── _index.tsx → /
├── about.tsx → /about
├── blog.tsx → /blog (layout)
├── blog._index.tsx → /blog (index, renders in blog layout)
├── blog.$slug.tsx → /blog/:slug (renders in blog layout)
├── blog.new.tsx → /blog/new
└── blog.edit.$id.tsx → /blog/edit/:id

مسارات التخطيط بدون مسار​

استخدم بادئة الشرطة السفلية لمسارات التخطيط التي لا تضيف مقطع URL:

app/routes/
├── _auth.tsx → Layout wrapper (no URL segment)
├── _auth.login.tsx → /login (renders inside _auth layout)
├── _auth.register.tsx → /register (renders inside _auth layout)
└── _auth.forgot-password.tsx → /forgot-password

مجموعات المسارات بأقواس​

استخدم الأقواس لتجميع المسارات دون التأثير على URL:

app/routes/
├── (marketing)/
│ ├── _index.tsx → /
│ ├── pricing.tsx → /pricing
│ └── features.tsx → /features
├── (app)/
│ ├── dashboard.tsx → /dashboard
│ └── settings.tsx → /settings
تلميح

اطلب من Cursor: "Generate the flat route file structure for a Remix app with public marketing pages (home, pricing, about), an authenticated app section (dashboard, settings, profile), and a blog with individual post pages." سيقوم Cursor بإنشاء جميع الملفات اللازمة باصطلاحات تسمية صحيحة.

أدوات التحميل والإجراءات​

تجلب أدوات التحميل البيانات على الخادم قبل عرض الصفحة. تعالج الإجراءات إرسالات النماذج والتعديلات. كلاهما يعمل على الخادم، مما يمنحك وصولاً مباشراً إلى قاعدة البيانات وواجهات API.

تعريف أداة تحميل​

// app/routes/blog.$slug.tsx
import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { getPost } from '~/models/post.server';

export const meta: MetaFunction<typeof loader> = ({ data }) => {
if (!data?.post) {
return [{ title: 'Post not found' }];
}
return [
{ title: data.post.title },
{ name: 'description', content: data.post.excerpt },
];
};

export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.slug!);

if (!post) {
throw new Response('Not Found', { status: 404 });
}

return json({ post });
}

export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();

return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
warning

حدد دائماً أنواع useLoaderData باستخدام typeof loader. هذا يمنحك IntelliSense كاملاً لشكل البيانات في مكونك. بدون المعامل العام، لا يستطيع Cursor توفير إكمال تلقائي دقيق لـ post.title و post.content، إلخ.

تعريف إجراء​

تعالج الإجراءات طلبات غير GET (POST، PUT، DELETE، PATCH). تقترن بشكل طبيعي مع نماذج HTML:

// app/routes/blog.$slug.tsx (continued)
import { Form, redirect, useActionData } from '@remix-run/react';
import type { ActionFunctionArgs } from '@remix-run/node';
import { createComment } from '~/models/comment.server';

interface ActionData {
errors?: {
name?: string;
body?: string;
};
}

export async function action({ params, request }: ActionFunctionArgs) {
const formData = await request.formData();
const name = formData.get('name');
const body = formData.get('body');

const errors: ActionData['errors'] = {};

if (!name || typeof name !== 'string' || name.trim().length < 2) {
errors.name = 'Name must be at least 2 characters';
}

if (!body || typeof body !== 'string' || body.trim().length < 5) {
errors.body = 'Comment must be at least 5 characters';
}

if (Object.keys(errors).length > 0) {
return json<ActionData>({ errors }, { status: 400 });
}

await createComment({
postId: params.slug!,
authorName: name as string,
body: body as string,
});

return redirect(`/blog/${params.slug}`);
}

export default function BlogPost() {
const actionData = useActionData<typeof action>();

return (
<article>
{/* Post content */}
<section>
<h2>Leave a comment</h2>
<Form method="post">
<div>
<label htmlFor="name">Name</label>
<input type="text" name="name" id="name" />
{actionData?.errors?.name && (
<p className="error">{actionData.errors.name}</p>
)}
</div>
<div>
<label htmlFor="body">Comment</label>
<textarea name="body" id="body" rows={4} />
{actionData?.errors?.body && (
<p className="error">{actionData.errors.body}</p>
)}
</div>
<button type="submit">Submit</button>
</Form>
</section>
</article>
);
}
تلميح

اطلب من Cursor: "Create a Remix route with a loader that fetches a list of products from my Prisma database, and an action that handles a search form with query and category filters." سيقوم Cursor بإنشاء أداة التحميل والإجراء وواجهة النموذج مع تحقق مناسب.

إجراءات متعددة لكل مسار​

استخدم حقول intent للتعامل مع إجراءات متعددة في مسار واحد:

export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get('intent');

switch (intent) {
case 'create':
return handleCreate(formData);
case 'update':
return handleUpdate(formData);
case 'delete':
return handleDelete(formData);
default:
return json({ error: 'Unknown intent' }, { status: 400 });
}
}
<Form method="post">
<input type="hidden" name="intent" value="create" />
<button type="submit">Create</button>
</Form>

<Form method="post">
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="id" value={item.id} />
<button type="submit">Delete</button>
</Form>

المسارات المتداخلة​

تتيح لك المسارات المتداخلة تركيب الصفحات من تخطيطات أب ومحتوى ابن. يعرض مسار الأب <Outlet /> حيث تظهر المسارات الفرعية.

إنشاء تخطيط مع Outlet​

// app/routes/blog.tsx
import { Outlet, NavLink, useLoaderData } from '@remix-run/react';
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { getCategories } from '~/models/category.server';

export async function loader({}: LoaderFunctionArgs) {
const categories = await getCategories();
return json({ categories });
}

export default function BlogLayout() {
const { categories } = useLoaderData<typeof loader>();

return (
<div className="blog-layout">
<nav className="sidebar">
<h2>Categories</h2>
<ul>
{categories.map((cat) => (
<li key={cat.id}>
<NavLink to={`/blog/category/${cat.slug}`}>
{cat.name}
</NavLink>
</li>
))}
</ul>
</nav>
<main className="content">
<Outlet />
</main>
</div>
);
}
warning

إذا كان مسارك المتداخل يعرض صفحة فارغة، تحقق من أن التخطيط الأب يتضمن مكوّن <Outlet />. بدونه، لن يجد محتوى المسار الفرعي مكاناً للعرض.

تحميل البيانات المتداخل​

كل مسار في هيكل متداخل يُشغّل أداة تحميله بالتوازي. هذا يعني أن أدوات تحميل الأب والابن تنفذ في وقت واحد، مما يحسن الأداء المُدرك:

// app/routes/blog.$slug.tsx
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.slug!);
return json({ post });
}

// app/routes/blog.tsx (parent)
export async function loader() {
const categories = await getCategories();
return json({ categories });
}

واجهة المستخدم المتفائلة مع useNavigation​

تتبع حالة التنقل لإظهار واجهة مستخدم متفائلة أثناء الإرسال:

import { useNavigation, Form } from '@remix-run/react';

export default function CreatePost() {
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';

return (
<Form method="post">
<input name="title" placeholder="Title" disabled={isSubmitting} />
<textarea name="content" placeholder="Content" disabled={isSubmitting} />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create Post'}
</button>
</Form>
);
}
تلميح

اطلب من Cursor: "Add optimistic UI to my Remix form so the submit button shows a loading state and the form fields are disabled during submission." سيقوم Cursor بربط useNavigation ومعالجة الحالات المعطلة بشكل صحيح.

حدود الأخطاء​

قدّم Remix v2 حدود أخطاء لكل مسار. إذا ألقى مسار خطأً، فإن حد خطأ ذلك المسار فقط يُعرض -- بقية الصفحة تبقى سليمة.

حد خطأ على مستوى المسار​

// app/routes/blog.$slug.tsx
import { isRouteErrorResponse, useRouteError } from '@remix-run/react';

export function ErrorBoundary() {
const error = useRouteError();

if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return (
<div>
<h1>Post not found</h1>
<p>The blog post you are looking for does not exist.</p>
<a href="/blog">Back to blog</a>
</div>
);
}
return (
<div>
<h1>Error {error.status}</h1>
<p>{error.statusText}</p>
</div>
);
}

if (error instanceof Error) {
return (
<div>
<h1>Unexpected Error</h1>
<p>{error.message}</p>
</div>
);
}

return <h1>Unknown error occurred</h1>;
}
warning

يجب تصدير حدود الأخطاء كتصديرات مسماة (export function ErrorBoundary). التصديرات الافتراضية لمكون المسار فقط. إذا صدّرتها كافتراضية، لن يلتقطها Remix كحد خطأ.

حد خطأ الجذر​

يجب أن يُصدّر التخطيط الجذري (app/root.tsx) أيضاً ErrorBoundary لالتقاط الأخطاء التي تتصاعد:

// app/root.tsx
import { Links, Meta, Outlet, Scripts, useRouteError } from '@remix-run/react';

export function ErrorBoundary() {
const error = useRouteError();

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Application Error</title>
<Links />
</head>
<body>
<main>
<h1>Something went wrong</h1>
<p>Please try refreshing the page.</p>
</main>
<Scripts />
</body>
</html>
);
}

معالجة الأخطاء في أدوات التحميل​

export async function loader({ params }: LoaderFunctionArgs) {
const user = await getUser(params.id);

if (!user) {
throw new Response('User not found', {
status: 404,
statusText: 'Not Found',
});
}

const posts = await getUserPosts(user.id);

if (posts.length === 0) {
throw new Response('No posts found', {
status: 404,
});
}

return json({ user, posts });
}
تلميح

اطلب من Cursor: "Add comprehensive error handling to my Remix route. Include a 404 response when the resource is not found, a 500 for database errors, and a route-level error boundary with appropriate UI for each case." سيقوم Cursor بإنشاء منطق خطأ أداة التحميل ومكوّن حد الخطأ.

إنشاء المسارات بمساعدة الذكاء الاصطناعي في Cursor​

يمكن لمساعد الذكاء الاصطناعي في Cursor إنشاء وحدات مسارات كاملة -- بما في ذلك أدوات التحميل والإجراءات والمكونات ودوال meta وحدود الأخطاء -- من أمر واحد.

إنشاء مسار كامل من أمر​

افتح Cursor Chat (Ctrl+L) وصف المسار:

Create a Remix v2 route at app/routes/tasks.$id.tsx that:
1. Loads a single task from Prisma by ID in the loader
2. Has an action to update the task (title, description, status)
3. Shows a form pre-filled with the task data
4. Validates inputs: title (min 3 chars), description (min 10 chars)
5. Includes a meta function with the task title
6. Has an error boundary for 404 (task not found)
7. Redirects to /tasks after successful update

سيقوم Cursor بإنشاء ملف مسار كامل مع أداة التحميل والإجراء ومكون النموذج ودالة meta وحد الخطأ. سيستخدم الكود المُنشأ واجهة برمجة تطبيقات Remix v2 (LoaderFunctionArgs و ActionFunctionArgs) ويتضمن تحقق النموذج المناسب مع رسائل أخطاء مضمنة، وقائمة منسدلة للحالة مُحددة مسبقاً بقيمة المهمة الحالية، وإعادة توجيه إلى /tasks عند النجاح.

تلميح

عند طلب إنشاء مسارات من Cursor، حدد دائماً إصدار Remix ("v2") ومسار ملف المسار المسطح. هذا يضمن استخدام Cursor لواجهة برمجة التطبيقات الصحيحة (مثلاً، LoaderFunctionArgs بدلاً من LoaderFunction المُهمَل مع params).

مسارات الموارد بمساعدة الذكاء الاصطناعي​

تُرجع مسارات الموارد بيانات دون عرض واجهة مستخدم -- مثالية لنقاط نهاية API:

Create a Remix resource route at app/routes/api.health.tsx that returns
JSON with status "ok", server uptime, and current timestamp. Support GET only.
// app/routes/api.health.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';

export async function loader({}: LoaderFunctionArgs) {
return json({
status: 'ok',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
});
}

الاختبار والنشر​

النشر على Vercel​

ثبّت محول Vercel:

npm install @remix-run/vercel

كوّن vite.config.ts:

import { vitePlugin as remix } from '@remix-run/dev';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
plugins: [
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
v3_singleFetch: true,
v3_lazyRouteDiscovery: true,
},
}),
tsconfigPaths(),
],
});
warning

قبل النشر، اختبر بناء الإنتاج محلياً باستخدام npm run build && npm start. الكود من جانب الخادم الذي يعمل في وضع التطوير قد يتصرف بشكل مختلف في الإنتاج -- خاصة الكود الذي يعتمد على متغيرات process.env أو وحدات Node.js المدمجة التي قد لا تدعمها منصة النشر.

الخلاصة​

يُنشئ اقتران Remix v2 مع Cursor بيئة تطوير كاملة المكدس قوية. تركيز إطار العمل على معايير الويب والتحسين التدريجي والتوجيه المتداخل يرتبط بشكل نظيف بنظام الملفات، ويمكن لمساعد الذكاء الاصطناعي في Cursor إنشاء مسارات كاملة -- بما في ذلك أدوات التحميل والإجراءات وحدود الأخطاء والنماذج -- من أوصاف اللغة الطبيعية. ابدأ باصطلاحات المسارات المسطحة، ابنِ طبقة البيانات الخاصة بك بأدوات تحميل مُحددة الأنواع وإجراءات مُتحقّق منها، أضف تخطيطات متداخلة مع <Outlet />، ودع Cursor يُنشئ الكود المتكرر لتركز على منطق عمل تطبيقك.