@evjs/cli 0.0.1-rc.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT.md +121 -0
- package/README.md +67 -0
- package/dist/config.js +35 -0
- package/dist/create-webpack-config.js +99 -0
- package/dist/index.js +220 -0
- package/dist/load-config.js +39 -0
- package/package.json +75 -0
- package/templates/basic-csr/index.html +11 -0
- package/templates/basic-csr/package.json +21 -0
- package/templates/basic-csr/src/main.tsx +22 -0
- package/templates/basic-csr/src/pages/__root.tsx +28 -0
- package/templates/basic-csr/src/pages/about.tsx +19 -0
- package/templates/basic-csr/src/pages/home.tsx +19 -0
- package/templates/basic-csr/src/pages/posts/index.tsx +63 -0
- package/templates/basic-csr/tsconfig.json +17 -0
- package/templates/basic-server-fns/index.html +11 -0
- package/templates/basic-server-fns/package.json +21 -0
- package/templates/basic-server-fns/src/api/users.server.ts +31 -0
- package/templates/basic-server-fns/src/main.tsx +12 -0
- package/templates/basic-server-fns/src/routes.tsx +108 -0
- package/templates/basic-server-fns/tsconfig.json +16 -0
- package/templates/complex-routing/index.html +11 -0
- package/templates/complex-routing/package.json +21 -0
- package/templates/complex-routing/src/api/data.server.ts +86 -0
- package/templates/complex-routing/src/main.tsx +29 -0
- package/templates/complex-routing/src/pages/__root.tsx +60 -0
- package/templates/complex-routing/src/pages/catch.tsx +26 -0
- package/templates/complex-routing/src/pages/dashboard.tsx +81 -0
- package/templates/complex-routing/src/pages/home.tsx +35 -0
- package/templates/complex-routing/src/pages/posts/index.tsx +104 -0
- package/templates/complex-routing/src/pages/search.tsx +71 -0
- package/templates/complex-routing/src/pages/user.tsx +32 -0
- package/templates/complex-routing/tsconfig.json +13 -0
- package/templates/configured-server-fns/ev.config.ts +56 -0
- package/templates/configured-server-fns/index.html +11 -0
- package/templates/configured-server-fns/package.json +21 -0
- package/templates/configured-server-fns/src/api/users.server.ts +22 -0
- package/templates/configured-server-fns/src/main.tsx +12 -0
- package/templates/configured-server-fns/src/routes.tsx +111 -0
- package/templates/configured-server-fns/tsconfig.json +16 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createRoute, Outlet, query } from "@evjs/runtime/client";
|
|
2
|
+
import { getStats } from "../api/data.server";
|
|
3
|
+
import { rootRoute } from "./__root";
|
|
4
|
+
|
|
5
|
+
const styles = {
|
|
6
|
+
card: {
|
|
7
|
+
border: "1px solid #e5e7eb",
|
|
8
|
+
borderRadius: 8,
|
|
9
|
+
padding: "1rem",
|
|
10
|
+
marginBottom: "0.75rem",
|
|
11
|
+
},
|
|
12
|
+
stat: { textAlign: "center" as const, padding: "1rem" },
|
|
13
|
+
tag: {
|
|
14
|
+
display: "inline-block",
|
|
15
|
+
background: "#f3f4f6",
|
|
16
|
+
borderRadius: 4,
|
|
17
|
+
padding: "2px 8px",
|
|
18
|
+
fontSize: 12,
|
|
19
|
+
marginRight: 4,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ── Pathless layout (no URL segment, just shared UI) ──
|
|
24
|
+
|
|
25
|
+
export const dashboardLayout = createRoute({
|
|
26
|
+
getParentRoute: () => rootRoute,
|
|
27
|
+
id: "dashboard-layout",
|
|
28
|
+
component: () => (
|
|
29
|
+
<div>
|
|
30
|
+
<Outlet />
|
|
31
|
+
</div>
|
|
32
|
+
),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ── Dashboard page (/dashboard) ──
|
|
36
|
+
|
|
37
|
+
function Dashboard() {
|
|
38
|
+
const { data: stats } = query(getStats).useQuery();
|
|
39
|
+
if (!stats) return <p>Loading...</p>;
|
|
40
|
+
return (
|
|
41
|
+
<div>
|
|
42
|
+
<h2>Dashboard</h2>
|
|
43
|
+
<div style={{ display: "flex", gap: "1rem" }}>
|
|
44
|
+
<div style={{ ...styles.card, ...styles.stat }}>
|
|
45
|
+
<div style={{ fontSize: 32, fontWeight: 700 }}>
|
|
46
|
+
{stats.totalPosts}
|
|
47
|
+
</div>
|
|
48
|
+
<div style={{ color: "#6b7280" }}>Posts</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div style={{ ...styles.card, ...styles.stat }}>
|
|
51
|
+
<div style={{ fontSize: 32, fontWeight: 700 }}>
|
|
52
|
+
{stats.totalUsers}
|
|
53
|
+
</div>
|
|
54
|
+
<div style={{ color: "#6b7280" }}>Users</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div style={{ ...styles.card, ...styles.stat }}>
|
|
57
|
+
<div style={{ fontSize: 32, fontWeight: 700 }}>
|
|
58
|
+
{stats.tags.length}
|
|
59
|
+
</div>
|
|
60
|
+
<div style={{ color: "#6b7280" }}>Tags</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<h3>All Tags</h3>
|
|
64
|
+
<div>
|
|
65
|
+
{stats.tags.map((t) => (
|
|
66
|
+
<span key={t} style={styles.tag}>
|
|
67
|
+
{t}
|
|
68
|
+
</span>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const dashboardRoute = createRoute({
|
|
76
|
+
getParentRoute: () => dashboardLayout,
|
|
77
|
+
path: "/dashboard",
|
|
78
|
+
loader: ({ context }) =>
|
|
79
|
+
context.queryClient.ensureQueryData(query(getStats).queryOptions()),
|
|
80
|
+
component: Dashboard,
|
|
81
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createRoute, Link } from "@evjs/runtime/client";
|
|
2
|
+
import { rootRoute } from "./__root";
|
|
3
|
+
|
|
4
|
+
export const homeRoute = createRoute({
|
|
5
|
+
getParentRoute: () => rootRoute,
|
|
6
|
+
path: "/",
|
|
7
|
+
component: () => (
|
|
8
|
+
<div>
|
|
9
|
+
<h1>evjs Complex Routing Example</h1>
|
|
10
|
+
<p>
|
|
11
|
+
Demonstrates nested layouts, dynamic params, search params, redirects,
|
|
12
|
+
and server function loaders.
|
|
13
|
+
</p>
|
|
14
|
+
<ul>
|
|
15
|
+
<li>
|
|
16
|
+
<Link to="/posts">Posts</Link> — nested group with dynamic{" "}
|
|
17
|
+
<code>$postId</code>
|
|
18
|
+
</li>
|
|
19
|
+
<li>
|
|
20
|
+
<Link to="/dashboard">Dashboard</Link> — pathless layout with server
|
|
21
|
+
function loader
|
|
22
|
+
</li>
|
|
23
|
+
<li>
|
|
24
|
+
<Link to="/search" search={{ q: "tutorial" }}>
|
|
25
|
+
Search
|
|
26
|
+
</Link>{" "}
|
|
27
|
+
— search params
|
|
28
|
+
</li>
|
|
29
|
+
<li>
|
|
30
|
+
<Link to="/old-blog">Old Blog</Link> — redirect to /posts
|
|
31
|
+
</li>
|
|
32
|
+
</ul>
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createRoute, Link, Outlet, query } from "@evjs/runtime/client";
|
|
2
|
+
import { getPost, getPosts } from "../../api/data.server";
|
|
3
|
+
import { rootRoute } from "../__root";
|
|
4
|
+
|
|
5
|
+
const styles = {
|
|
6
|
+
sidebar: { display: "flex", gap: "1.5rem" },
|
|
7
|
+
nav: {
|
|
8
|
+
minWidth: 180,
|
|
9
|
+
borderRight: "1px solid #e5e7eb",
|
|
10
|
+
paddingRight: "1rem",
|
|
11
|
+
},
|
|
12
|
+
tag: {
|
|
13
|
+
display: "inline-block",
|
|
14
|
+
background: "#f3f4f6",
|
|
15
|
+
borderRadius: 4,
|
|
16
|
+
padding: "2px 8px",
|
|
17
|
+
fontSize: 12,
|
|
18
|
+
marginRight: 4,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ── Posts layout (/posts) ──
|
|
23
|
+
|
|
24
|
+
function PostsLayout() {
|
|
25
|
+
const { data: posts } = query(getPosts).useQuery();
|
|
26
|
+
return (
|
|
27
|
+
<div style={styles.sidebar}>
|
|
28
|
+
<div style={styles.nav}>
|
|
29
|
+
<h3 style={{ marginTop: 0 }}>Posts</h3>
|
|
30
|
+
<ul style={{ listStyle: "none", padding: 0 }}>
|
|
31
|
+
{posts?.map((p) => (
|
|
32
|
+
<li key={p.id} style={{ marginBottom: "0.25rem" }}>
|
|
33
|
+
<Link
|
|
34
|
+
to="/posts/$postId"
|
|
35
|
+
params={{ postId: p.id }}
|
|
36
|
+
style={{ textDecoration: "none", color: "#374151" }}
|
|
37
|
+
activeProps={{ style: { fontWeight: "bold", color: "#111" } }}
|
|
38
|
+
>
|
|
39
|
+
{p.title}
|
|
40
|
+
</Link>
|
|
41
|
+
</li>
|
|
42
|
+
))}
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
45
|
+
<div style={{ flex: 1 }}>
|
|
46
|
+
<Outlet />
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const postsRoute = createRoute({
|
|
53
|
+
getParentRoute: () => rootRoute,
|
|
54
|
+
path: "/posts",
|
|
55
|
+
component: PostsLayout,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── Posts index (/posts/) ──
|
|
59
|
+
|
|
60
|
+
export const postsIndexRoute = createRoute({
|
|
61
|
+
getParentRoute: () => postsRoute,
|
|
62
|
+
path: "/",
|
|
63
|
+
component: () => (
|
|
64
|
+
<p style={{ color: "#6b7280" }}>← Select a post from the sidebar</p>
|
|
65
|
+
),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── Post detail (/posts/$postId) ──
|
|
69
|
+
|
|
70
|
+
function PostDetail() {
|
|
71
|
+
const { postId } = postDetailRoute.useParams();
|
|
72
|
+
const { data: post } = query(getPost).useQuery(postId);
|
|
73
|
+
|
|
74
|
+
if (!post) return <p>Loading...</p>;
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<h2>{post.title}</h2>
|
|
78
|
+
<p style={{ color: "#6b7280" }}>
|
|
79
|
+
by{" "}
|
|
80
|
+
<Link to="/users/$username" params={{ username: post.author }}>
|
|
81
|
+
{post.author}
|
|
82
|
+
</Link>
|
|
83
|
+
</p>
|
|
84
|
+
<p>{post.body}</p>
|
|
85
|
+
<div>
|
|
86
|
+
{post.tags.map((tag) => (
|
|
87
|
+
<span key={tag} style={styles.tag}>
|
|
88
|
+
{tag}
|
|
89
|
+
</span>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const postDetailRoute = createRoute({
|
|
97
|
+
getParentRoute: () => postsRoute,
|
|
98
|
+
path: "$postId",
|
|
99
|
+
loader: ({ params, context }) =>
|
|
100
|
+
context.queryClient.ensureQueryData(
|
|
101
|
+
query(getPost).queryOptions(params.postId),
|
|
102
|
+
),
|
|
103
|
+
component: PostDetail,
|
|
104
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createRoute, Link, query } from "@evjs/runtime/client";
|
|
2
|
+
import { getPosts } from "../api/data.server";
|
|
3
|
+
import { rootRoute } from "./__root";
|
|
4
|
+
|
|
5
|
+
const styles = {
|
|
6
|
+
card: {
|
|
7
|
+
border: "1px solid #e5e7eb",
|
|
8
|
+
borderRadius: 8,
|
|
9
|
+
padding: "1rem",
|
|
10
|
+
marginBottom: "0.75rem",
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function SearchPage() {
|
|
15
|
+
const { q } = searchRoute.useSearch();
|
|
16
|
+
const { data: results } = query(getPosts).useQuery(q || undefined);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div>
|
|
20
|
+
<h2>Search</h2>
|
|
21
|
+
<form
|
|
22
|
+
onSubmit={(e) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
const form = new FormData(e.currentTarget);
|
|
25
|
+
const q = form.get("q") as string;
|
|
26
|
+
window.location.search = `?q=${encodeURIComponent(q)}`;
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<input
|
|
30
|
+
name="q"
|
|
31
|
+
defaultValue={q}
|
|
32
|
+
placeholder="Search posts..."
|
|
33
|
+
style={{
|
|
34
|
+
padding: "0.5rem",
|
|
35
|
+
width: 300,
|
|
36
|
+
borderRadius: 4,
|
|
37
|
+
border: "1px solid #d1d5db",
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
</form>
|
|
41
|
+
<div style={{ marginTop: "1rem" }}>
|
|
42
|
+
{q && <p style={{ color: "#6b7280" }}>Results for "{q}":</p>}
|
|
43
|
+
{results?.map((post) => (
|
|
44
|
+
<div key={post.id} style={styles.card}>
|
|
45
|
+
<Link
|
|
46
|
+
to="/posts/$postId"
|
|
47
|
+
params={{ postId: post.id }}
|
|
48
|
+
style={{ textDecoration: "none", color: "#111" }}
|
|
49
|
+
>
|
|
50
|
+
<strong>{post.title}</strong>
|
|
51
|
+
</Link>
|
|
52
|
+
<p
|
|
53
|
+
style={{ margin: "0.25rem 0 0", color: "#6b7280", fontSize: 14 }}
|
|
54
|
+
>
|
|
55
|
+
{post.body}
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const searchRoute = createRoute({
|
|
65
|
+
getParentRoute: () => rootRoute,
|
|
66
|
+
path: "/search",
|
|
67
|
+
validateSearch: (search: Record<string, unknown>) => ({
|
|
68
|
+
q: (search.q as string) || "",
|
|
69
|
+
}),
|
|
70
|
+
component: SearchPage,
|
|
71
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createRoute, Link, query } from "@evjs/runtime/client";
|
|
2
|
+
import { getUser } from "../api/data.server";
|
|
3
|
+
import { rootRoute } from "./__root";
|
|
4
|
+
|
|
5
|
+
const styles = {
|
|
6
|
+
card: { border: "1px solid #e5e7eb", borderRadius: 8, padding: "1rem" },
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function UserProfile() {
|
|
10
|
+
const { username } = userRoute.useParams();
|
|
11
|
+
const { data: user } = query(getUser).useQuery(username);
|
|
12
|
+
|
|
13
|
+
if (!user) return <p>Loading...</p>;
|
|
14
|
+
return (
|
|
15
|
+
<div style={styles.card}>
|
|
16
|
+
<h2>{user.name}</h2>
|
|
17
|
+
<p style={{ color: "#6b7280" }}>@{user.username}</p>
|
|
18
|
+
<p>{user.bio}</p>
|
|
19
|
+
<Link to="/posts">← Back to posts</Link>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const userRoute = createRoute({
|
|
25
|
+
getParentRoute: () => rootRoute,
|
|
26
|
+
path: "/users/$username",
|
|
27
|
+
loader: ({ params, context }) =>
|
|
28
|
+
context.queryClient.ensureQueryData(
|
|
29
|
+
query(getUser).queryOptions(params.username),
|
|
30
|
+
),
|
|
31
|
+
component: UserProfile,
|
|
32
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defineConfig } from "@evjs/cli";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advanced ev.config.ts example.
|
|
5
|
+
*
|
|
6
|
+
* This file demonstrates all available configuration options.
|
|
7
|
+
* All fields are optional — evjs works zero-config out of the box.
|
|
8
|
+
*/
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
client: {
|
|
11
|
+
// Client entry point (default: "./src/main.tsx")
|
|
12
|
+
entry: "./src/main.tsx",
|
|
13
|
+
|
|
14
|
+
// HTML template (default: "./index.html")
|
|
15
|
+
html: "./index.html",
|
|
16
|
+
|
|
17
|
+
// Dev server options (merged with built-in defaults)
|
|
18
|
+
dev: {
|
|
19
|
+
// Dev server port (default: 3000)
|
|
20
|
+
port: 4000,
|
|
21
|
+
|
|
22
|
+
// Any dev server options can be added here:
|
|
23
|
+
// https: true,
|
|
24
|
+
// open: true,
|
|
25
|
+
// historyApiFallback: true,
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// Transport configuration for server function calls
|
|
29
|
+
transport: {
|
|
30
|
+
// Base URL for server function endpoint (default: same origin)
|
|
31
|
+
// baseUrl: "https://api.example.com",
|
|
32
|
+
|
|
33
|
+
// Server function endpoint path (default: "/api/fn")
|
|
34
|
+
endpoint: "/api/fn",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
server: {
|
|
39
|
+
// Server function endpoint path (default: "/api/fn")
|
|
40
|
+
endpoint: "/api/fn",
|
|
41
|
+
|
|
42
|
+
// Server runner module (default: "@evjs/runtime/server/node")
|
|
43
|
+
// For Deno/Bun/Workers, use: "@evjs/runtime/server/ecma"
|
|
44
|
+
runner: "@evjs/runtime/server/node",
|
|
45
|
+
|
|
46
|
+
// Middleware module paths to auto-register in server entry
|
|
47
|
+
// These are imported and applied in order
|
|
48
|
+
// middleware: ["./src/middleware/auth.ts", "./src/middleware/cors.ts"],
|
|
49
|
+
|
|
50
|
+
// Dev server options
|
|
51
|
+
dev: {
|
|
52
|
+
// API server port in dev mode (default: 3001)
|
|
53
|
+
port: 4001,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Configured Server Functions Example</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example-configured-server-fns",
|
|
3
|
+
"version": "0.0.1-rc.8",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "ev dev",
|
|
7
|
+
"build": "ev build",
|
|
8
|
+
"check-types": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@evjs/runtime": "^0.0.1-rc.13",
|
|
12
|
+
"react": "^19.2.4",
|
|
13
|
+
"react-dom": "^19.2.4"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@evjs/cli": "^0.0.1-rc.5",
|
|
17
|
+
"@types/react": "^19.1.8",
|
|
18
|
+
"@types/react-dom": "^19.1.8",
|
|
19
|
+
"typescript": "^5.7.3"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
/** Simulated user database. */
|
|
4
|
+
const users = [
|
|
5
|
+
{ id: "1", name: "Alice", email: "alice@example.com" },
|
|
6
|
+
{ id: "2", name: "Bob", email: "bob@example.com" },
|
|
7
|
+
{ id: "3", name: "Charlie", email: "charlie@example.com" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
/** Get all users. */
|
|
11
|
+
export async function getUsers() {
|
|
12
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
13
|
+
return users;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Create a new user. */
|
|
17
|
+
export async function createUser(data: { name: string; email: string }) {
|
|
18
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
19
|
+
const newUser = { id: String(users.length + 1), ...data };
|
|
20
|
+
users.push(newUser);
|
|
21
|
+
return newUser;
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createApp } from "@evjs/runtime/client";
|
|
2
|
+
import { routeTree } from "./routes";
|
|
3
|
+
|
|
4
|
+
const app = createApp({ routeTree });
|
|
5
|
+
|
|
6
|
+
declare module "@tanstack/react-router" {
|
|
7
|
+
interface Register {
|
|
8
|
+
router: typeof app.router;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
app.render("#app");
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAppRootRoute,
|
|
3
|
+
createMutationProxy,
|
|
4
|
+
createQueryProxy,
|
|
5
|
+
createRoute,
|
|
6
|
+
Link,
|
|
7
|
+
Outlet,
|
|
8
|
+
useQueryClient,
|
|
9
|
+
} from "@evjs/runtime/client";
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import * as usersApi from "./api/users.server";
|
|
12
|
+
|
|
13
|
+
// ── API Proxy ──
|
|
14
|
+
|
|
15
|
+
const api = {
|
|
16
|
+
users: {
|
|
17
|
+
query: createQueryProxy(usersApi),
|
|
18
|
+
mutation: createMutationProxy(usersApi),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ── Root Route ──
|
|
23
|
+
|
|
24
|
+
function Root() {
|
|
25
|
+
return (
|
|
26
|
+
<div style={{ fontFamily: "system-ui, sans-serif", padding: "1rem" }}>
|
|
27
|
+
<h1>Configured Server Functions</h1>
|
|
28
|
+
<p style={{ color: "#666" }}>
|
|
29
|
+
This example uses <code>ev.config.ts</code> for custom ports and
|
|
30
|
+
settings.
|
|
31
|
+
</p>
|
|
32
|
+
<nav style={{ display: "flex", gap: "1rem", marginBottom: "1rem" }}>
|
|
33
|
+
<Link to="/">Users</Link>
|
|
34
|
+
</nav>
|
|
35
|
+
<Outlet />
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rootRoute = createAppRootRoute({ component: Root });
|
|
41
|
+
|
|
42
|
+
// ── Users Route ──
|
|
43
|
+
|
|
44
|
+
function UsersPage() {
|
|
45
|
+
const [name, setName] = useState("");
|
|
46
|
+
const [email, setEmail] = useState("");
|
|
47
|
+
|
|
48
|
+
const { data: users = [], isLoading } = api.users.query.getUsers.useQuery();
|
|
49
|
+
|
|
50
|
+
const queryClient = useQueryClient();
|
|
51
|
+
const { mutateAsync: doCreateUser } =
|
|
52
|
+
api.users.mutation.createUser.useMutation({
|
|
53
|
+
onSuccess: () => {
|
|
54
|
+
queryClient.invalidateQueries({
|
|
55
|
+
queryKey: api.users.query.getUsers.queryKey(),
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
async function handleCreate(e: { preventDefault: () => void }) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
if (!name || !email) return;
|
|
63
|
+
await doCreateUser([{ name, email }]);
|
|
64
|
+
setName("");
|
|
65
|
+
setEmail("");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isLoading) return <p>Loading users from server…</p>;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div>
|
|
72
|
+
<h2>Users (via Query Proxy)</h2>
|
|
73
|
+
<ul>
|
|
74
|
+
{users.map((u: { id: string; name: string; email: string }) => (
|
|
75
|
+
<li key={u.id}>
|
|
76
|
+
{u.name} — {u.email}
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
|
|
81
|
+
<h3>Add User</h3>
|
|
82
|
+
<form onSubmit={handleCreate} style={{ display: "flex", gap: "0.5rem" }}>
|
|
83
|
+
<input
|
|
84
|
+
placeholder="Name"
|
|
85
|
+
value={name}
|
|
86
|
+
onChange={(e) => setName(e.target.value)}
|
|
87
|
+
/>
|
|
88
|
+
<input
|
|
89
|
+
placeholder="Email"
|
|
90
|
+
value={email}
|
|
91
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
92
|
+
/>
|
|
93
|
+
<button type="submit">Create</button>
|
|
94
|
+
</form>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const usersRoute = createRoute({
|
|
100
|
+
getParentRoute: () => rootRoute,
|
|
101
|
+
path: "/",
|
|
102
|
+
component: UsersPage,
|
|
103
|
+
loader: ({ context }) =>
|
|
104
|
+
context.queryClient.ensureQueryData(
|
|
105
|
+
api.users.query.getUsers.queryOptions(),
|
|
106
|
+
),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ── Route Tree ──
|
|
110
|
+
|
|
111
|
+
export const routeTree = rootRoute.addChildren([usersRoute]);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["src"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
8
|
+
"moduleResolution": "Bundler",
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"verbatimModuleSyntax": true
|
|
15
|
+
}
|
|
16
|
+
}
|