~repos /edge-city

#react#js#ssr

git clone https://pyrossh.dev/repos/edge-city.git

edge-city is a next level meta-framework for react that runs only on edge runtimes


e6ce5bd9 Peter John

2 years ago
implement rpc
packages/example/bun.lockb CHANGED
Binary file
packages/example/containers/TodoList/TodoList.jsx CHANGED
@@ -1,20 +1,16 @@
1
1
  import React from 'react';
2
2
  import Todo from "@/components/Todo/Todo";
3
-
4
- const todos = [
5
- { id: '1', text: "123" },
3
+ import { getTodos } from "@/services/todos.service";
6
- { id: '2', text: "ABC" }
4
+ import { useRpc } from "parotta/rpc";
7
- ];
8
5
 
9
6
  const TodoList = () => {
10
- // const { data: todos } = useFetch("/todos");
7
+ const { data } = useRpc(getTodos, {});
11
8
  return (
12
9
  <div className="todo-list">
13
- <h1>Todos</h1>
14
10
  <ul>
15
- {todos.map((item) => (
11
+ {data.map((item) => (
16
- <li key={item.text}>
12
+ <li key={item.id}>
17
- <Todo id={item.id} text={item.text} />
13
+ <Todo todo={item} />
18
14
  </li>
19
15
  ))}
20
16
  </ul>
@@ -22,4 +18,4 @@ const TodoList = () => {
22
18
  );
23
19
  }
24
20
 
25
- export default TodoList;
21
+ export default TodoList;
packages/example/{routes → pages}/404/page.css RENAMED
File without changes
packages/example/{routes → pages}/404/page.jsx RENAMED
File without changes
packages/example/{routes → pages}/about/page.css RENAMED
File without changes
packages/example/{routes → pages}/about/page.jsx RENAMED
File without changes
packages/example/{routes → pages}/layout.css RENAMED
File without changes
packages/example/{routes → pages}/layout.jsx RENAMED
@@ -2,7 +2,6 @@ import React, { Suspense } from 'react';
2
2
  import { Link } from "parotta/router";
3
3
  import { ErrorBoundary } from "parotta/error";
4
4
  import "./layout.css";
5
- // import '@blueprintjs/core/lib/css/blueprint.css';
6
5
 
7
6
  const Layout = ({ children }) => {
8
7
  return (
packages/example/{routes → pages}/page.css RENAMED
File without changes
packages/example/{routes → pages}/page.jsx RENAMED
File without changes
packages/example/{routes → pages}/page.spec.js RENAMED
File without changes
packages/example/{routes → pages}/todos/page.css RENAMED
File without changes
packages/example/{routes → pages}/todos/page.jsx RENAMED
@@ -1,6 +1,7 @@
1
1
  import React, { useEffect } from 'react';
2
2
  import { useFetch } from "parotta/fetch";
3
- import Todo from "@/components/Todo/Todo";
3
+ // import Todo from "@/components/Todo/Todo";
4
+ import TodoList from "@/containers/TodoList/TodoList";
4
5
  import "./page.css";
5
6
 
6
7
  export const Head = () => {
@@ -10,21 +11,22 @@ export const Head = () => {
10
11
  }
11
12
 
12
13
  export const Body = () => {
13
- const { data, cache } = useFetch("/api/todos");
14
+ // const { data, cache } = useFetch("/api/todos");
14
- useEffect(() => {
15
+ // useEffect(() => {
15
- setTimeout(() => {
16
+ // setTimeout(() => {
16
- cache.invalidate(/todos/);
17
+ // cache.invalidate(/todos/);
17
- }, 3000)
18
+ // }, 3000)
18
- }, []);
19
+ // }, []);
19
20
  return (
20
21
  <div>
21
22
  <h1>Todos</h1>
22
23
  <ul>
23
- {data?.map((item) => (
24
+ {/* {data?.map((item) => (
24
25
  <li key={item.id}>
25
26
  <Todo todo={item} />
26
27
  </li>
27
- ))}
28
+ ))} */}
29
+ <TodoList />
28
30
  </ul>
29
31
  </div>
30
32
  )
packages/example/routes/api/auth/index.js DELETED
@@ -1,54 +0,0 @@
1
- import NextAuth from "next-auth";
2
- import EmailProvider from "next-auth/providers/email";
3
- import GoogleProvider from "next-auth/providers/google";
4
- import DrizzleAuthAdapterPG from "drizzle-auth-adaptor-pg";
5
- import db from "@/db";
6
-
7
- // GET /api/auth/signin
8
- // POST /api/auth/signin/:provider
9
- // GET/POST /api/auth/callback/:provider
10
- // GET /api/auth/signout
11
- // POST /api/auth/signout
12
- // GET /api/auth/session
13
- // GET /api/auth/csrf
14
- // GET /api/auth/providers
15
-
16
- // NEXTAUTH_SECRET="This is an example"
17
- // NEXTAUTH_URL
18
-
19
- // import { SessionProvider } from "next-auth/react"
20
- // export default function App({
21
- // Component,
22
- // pageProps: { session, ...pageProps },
23
- // }) {
24
- // return (
25
- // <SessionProvider session={session}>
26
- // <Component {...pageProps} />
27
- // </SessionProvider>
28
- // )
29
- // }
30
-
31
- const handler = NextAuth({
32
- adapter: DrizzleAuthAdapterPG(db),
33
- providers: [
34
- EmailProvider({
35
- server: {
36
- host: process.env.SMTP_HOST,
37
- port: Number(process.env.SMTP_PORT),
38
- auth: {
39
- user: process.env.SMTP_USER,
40
- pass: process.env.SMTP_PASSWORD,
41
- },
42
- },
43
- from: process.env.EMAIL_FROM,
44
- }),
45
- GoogleProvider({
46
- clientId: process.env.GOOGLE_CLIENT_ID,
47
- clientSecret: process.env.GOOGLE_CLIENT_SECRET,
48
- }),
49
- ],
50
- });
51
-
52
- export const onGet = handler
53
-
54
- export const onPost = handler
packages/example/routes/api/index.js DELETED
@@ -1,8 +0,0 @@
1
- export const onGet = () => {
2
- return new Response("ok", {
3
- headers: {
4
- "Content-Type": "application/json",
5
- },
6
- status: 200,
7
- });
8
- }
packages/example/routes/api/todos/index.js DELETED
@@ -1,50 +0,0 @@
1
- import { gt, eq } from 'drizzle-orm';
2
- import db, { todos } from "@/db";
3
-
4
- export const onGet = async (req) => {
5
- const url = new URL(req.url);
6
- const page = parseInt(url.searchParams.get("page") || "1", 10);
7
- const results = await db.select().from(todos).where(gt(todos.id, 0)).limit(page * 5).offset((page - 1) * 5);
8
- return new Response(JSON.stringify(results), {
9
- headers: {
10
- "Content-Type": "application/json",
11
- },
12
- status: 200,
13
- });
14
- }
15
-
16
- export const onPost = async (req) => {
17
- const body = await req.body();
18
- const input = JSON.parse(body);
19
- const data = await db.insert(todos).values(input).returning();
20
- return new Response(JSON.stringify(data), {
21
- headers: {
22
- "Content-Type": "application/json",
23
- },
24
- status: 200,
25
- });
26
- }
27
-
28
- export const onPatch = async (req) => {
29
- const body = await req.body();
30
- const input = JSON.parse(body);
31
- const data = await db.update(todos).set(input).where(eq(todos.id, input.id)).returning();
32
- return new Response(JSON.stringify(data), {
33
- headers: {
34
- "Content-Type": "application/json",
35
- },
36
- status: 200,
37
- });
38
- }
39
-
40
- export const onDelete = async (req) => {
41
- const url = new URL(req.url);
42
- const id = url.searchParams.get("id");
43
- const data = await db.delete(todos).where(eq(todos.id, id)).returning();
44
- return new Response(JSON.stringify(data), {
45
- headers: {
46
- "Content-Type": "application/json",
47
- },
48
- status: 200,
49
- });
50
- }
packages/example/services/auth.service.js ADDED
@@ -0,0 +1,50 @@
1
+ // import NextAuth from "next-auth";
2
+ // import EmailProvider from "next-auth/providers/email";
3
+ // import GoogleProvider from "next-auth/providers/google";
4
+ // import DrizzleAuthAdapterPG from "drizzle-auth-adaptor-pg";
5
+ // import db from "@/db";
6
+
7
+ // GET /api/auth/signin
8
+ // POST /api/auth/signin/:provider
9
+ // GET/POST /api/auth/callback/:provider
10
+ // GET /api/auth/signout
11
+ // POST /api/auth/signout
12
+ // GET /api/auth/session
13
+ // GET /api/auth/csrf
14
+ // GET /api/auth/providers
15
+
16
+ // NEXTAUTH_SECRET="This is an example"
17
+ // NEXTAUTH_URL
18
+
19
+ // import { SessionProvider } from "next-auth/react"
20
+ // export default function App({
21
+ // Component,
22
+ // pageProps: { session, ...pageProps },
23
+ // }) {
24
+ // return (
25
+ // <SessionProvider session={session}>
26
+ // <Component {...pageProps} />
27
+ // </SessionProvider>
28
+ // )
29
+ // }
30
+
31
+ // const handler = NextAuth({
32
+ // adapter: DrizzleAuthAdapterPG(db),
33
+ // providers: [
34
+ // EmailProvider({
35
+ // server: {
36
+ // host: process.env.SMTP_HOST,
37
+ // port: Number(process.env.SMTP_PORT),
38
+ // auth: {
39
+ // user: process.env.SMTP_USER,
40
+ // pass: process.env.SMTP_PASSWORD,
41
+ // },
42
+ // },
43
+ // from: process.env.EMAIL_FROM,
44
+ // }),
45
+ // GoogleProvider({
46
+ // clientId: process.env.GOOGLE_CLIENT_ID,
47
+ // clientSecret: process.env.GOOGLE_CLIENT_SECRET,
48
+ // }),
49
+ // ],
50
+ // });
packages/example/services/todos.service.js ADDED
@@ -0,0 +1,25 @@
1
+ import { eq, asc } from 'drizzle-orm';
2
+ import db, { todos } from "@/db";
3
+
4
+ export const getTodos = async () => {
5
+ // console.log("getTodos");
6
+ // return [];
7
+ return await db.select().from(todos).orderBy(asc(todos.id));
8
+ }
9
+
10
+ export const createTodo = async (item) => {
11
+ return await db.insert(todos).values(item).returning();
12
+ }
13
+
14
+ export const getTodo = async (id) => {
15
+ const results = await db.select().from(todos).where(eq(todos.id, id));
16
+ return results[0]
17
+ }
18
+
19
+ export const updateTodo = async (item) => {
20
+ return await db.update(todos).set(item).where(eq(todos.id, item.id)).returning();
21
+ }
22
+
23
+ export const deleteTodo = async (id) => {
24
+ return await db.delete(todos).where(eq(todos.id, id)).returning();
25
+ }
packages/parotta/cli.js CHANGED
@@ -24,14 +24,21 @@ console.log(`running with cwd=${path.basename(process.cwd())} node_env=${process
24
24
 
25
25
  const isProd = process.env.NODE_ENV === "production";
26
26
 
27
- const mapFiles = () => {
27
+ const mapServerRoutes = () => {
28
28
  const routes = {};
29
- const dirs = walkdir.sync(path.join(process.cwd(), "routes"))
29
+ const dirs = walkdir.sync(path.join(process.cwd(), "pages"))
30
30
  .map((s) => s.replace(process.cwd(), "")
31
- .replace("/routes", "")
31
+ .replace("/pages", "")
32
32
  // .replaceAll("[", ":")
33
33
  // .replaceAll("]", "")
34
34
  );
35
+ walkdir.sync(path.join(process.cwd(), "services"))
36
+ .map((s) => s.replace(process.cwd(), ""))
37
+ .filter((s) => s.includes(".service.js"))
38
+ .forEach((s) => {
39
+ const serviceName = s.replace(".service.js", "");
40
+ routes[serviceName + "/*"] = { key: serviceName, service: s };
41
+ });
35
42
  dirs.filter((p) => p.includes('page.jsx'))
36
43
  .map((s) => ({ path: s, route: s.replace("/page.jsx", "") }))
37
44
  .forEach((page) => {
@@ -44,14 +51,6 @@ const mapFiles = () => {
44
51
  const key = item.route || "/";
45
52
  routes[key].layout = item.path;
46
53
  });
47
- dirs.filter((p) => p.includes('index.js'))
48
- .map((s) => s.replace(process.cwd(), ""))
49
- .map((s) => ({ path: s, route: s.replace("/index.js", "") }))
50
- .forEach((api) => {
51
- const key = api.route || "/";
52
- routes[key] = routes[key] || { key };
53
- routes[key].api = api.path;
54
- });
55
54
  walkdir.sync(path.join(process.cwd(), "static"))
56
55
  .map((s) => s.replace(process.cwd(), "").replace("/static", ""))
57
56
  .forEach((route) => {
@@ -63,20 +62,25 @@ const mapFiles = () => {
63
62
  const mapDeps = (dir) => {
64
63
  return walkdir.sync(path.join(process.cwd(), dir))
65
64
  .map((s) => s.replace(process.cwd(), ""))
66
- .filter((s) => s.includes(".jsx"))
65
+ .filter((s) => s.includes(".jsx") || s.includes(".js"))
67
66
  .reduce((acc, s) => {
67
+ if (s.includes(".jsx")) {
68
- acc['@' + s.replace(".jsx", "")] = s
68
+ acc['@' + s.replace(".jsx", "")] = s
69
+ }
70
+ if (s.includes(".js")) {
71
+ acc['@' + s.replace(".js", "")] = s
72
+ }
69
73
  return acc;
70
74
  }, {});
71
75
  }
72
76
 
73
- const mapPages = () => walkdir.sync(path.join(process.cwd(), "routes"))
77
+ const mapPages = () => walkdir.sync(path.join(process.cwd(), "pages"))
74
78
  .filter((p) => p.includes('page.jsx'))
75
79
  .map((s) => s.replace(process.cwd(), ""))
76
- .map((s) => s.replace("/routes", ""))
80
+ .map((s) => s.replace("/pages", ""))
77
81
  .map((s) => s.replace("/page.jsx", ""));
78
82
 
79
- const serverSideRoutes = mapFiles();
83
+ const serverSideRoutes = mapServerRoutes();
80
84
  const clientSideRoutes = mapPages();
81
85
 
82
86
  const serverRouter = createRouter({
@@ -86,9 +90,9 @@ const serverRouter = createRouter({
86
90
 
87
91
  const clientRoutes = await clientSideRoutes.reduce(async (accp, r) => {
88
92
  const acc = await accp;
89
- const src = await import(`${process.cwd()}/routes${r}/page.jsx`);
93
+ const src = await import(`${process.cwd()}/pages${r}/page.jsx`);
90
- const exists = fs.existsSync(`${process.cwd()}/routes${r}/layout.jsx`);
94
+ const exists = fs.existsSync(`${process.cwd()}/pages${r}/layout.jsx`);
91
- const lpath = exists ? `/routes${r}/layout.jsx` : `/routes/layout.jsx`;
95
+ const lpath = exists ? `/pages${r}/layout.jsx` : `/pages/layout.jsx`;
92
96
  const lsrc = await import(`${process.cwd()}${lpath}`);
93
97
  acc[r === "" ? "/" : r] = {
94
98
  Head: src.Head,
@@ -106,29 +110,16 @@ const clientRouter = createRouter({
106
110
  routes: clientRoutes,
107
111
  });
108
112
 
109
- const renderApi = async (filePath, req) => {
113
+ const renderApi = async (key, filePath, req) => {
114
+ const url = new URL(req.url);
115
+ const params = await req.json();
116
+ const funcName = url.pathname.replace(`${key}/`, "");
110
- const routeImport = await import(path.join(process.cwd(), filePath));
117
+ const js = await import(path.join(process.cwd(), filePath));
111
- switch (req.method) {
112
- case "HEAD":
113
- return routeImport.onHead(req);
118
+ const result = await js[funcName](params);
114
- case "OPTIONS":
115
- return routeImport.onOptions(req);
119
+ return new Response(JSON.stringify(result), {
116
- case "GET":
117
- return routeImport.onGet(req);
118
- case "POST":
119
- return routeImport.onPost(req);
120
- case "PUT":
121
- return routeImport.onPut(req);
122
- case "PATCH":
123
- return routeImport.onPatch(req);
124
- case "DELETE":
125
- return routeImport.onDelete(req);
126
- default:
127
- return new Response(`{"message": "route not found"}`, {
128
- headers: { 'Content-Type': 'application/json' },
120
+ headers: { 'Content-Type': 'application/json' },
129
- status: 404,
121
+ status: 200,
130
- });
122
+ });
131
- }
132
123
  }
133
124
 
134
125
  console.log(clientRoutes)
@@ -157,6 +148,7 @@ const renderPage = async (url) => {
157
148
  "parotta/router": `/parotta/router.js`,
158
149
  "parotta/error": `/parotta/error.js`,
159
150
  "parotta/fetch": `/parotta/fetch.js`,
151
+ "parotta/rpc": `/parotta/rpc.js`,
160
152
  ...nodeDeps,
161
153
  ...components,
162
154
  ...containers,
@@ -191,8 +183,8 @@ const radixRouter = createRouter({
191
183
  strictTrailingSlash: true,
192
184
  routes: {
193
185
  ${Object.keys(clientRoutes).map((r) => `"${r}": {
194
- Head: React.lazy(() => import("/routes${r}/page.jsx").then((js) => ({ default: js.Head }))),
186
+ Head: React.lazy(() => import("/pages${r}/page.jsx").then((js) => ({ default: js.Head }))),
195
- Body: React.lazy(() => import("/routes${r}/page.jsx").then((js) => ({ default: js.Body }))),
187
+ Body: React.lazy(() => import("/pages${r}/page.jsx").then((js) => ({ default: js.Body }))),
196
188
  Layout: React.lazy(() => import("${clientRoutes[r].LayoutPath}")),
197
189
  LayoutPath: "${clientRoutes[r].LayoutPath}",
198
190
  }`).join(',\n ')}
@@ -255,7 +247,27 @@ const renderJs = async (src) => {
255
247
  try {
256
248
  const jsText = await Bun.file(src).text();
257
249
  const result = await transpiler.transform(jsText);
250
+ // inject code which calls the api for that function
251
+ const lines = result.split("\n");
252
+
253
+ // replace all .service imports which rpc interface
254
+ let addRpcImport = false;
255
+ lines.forEach((ln) => {
256
+ if (ln.includes(".service")) {
257
+ addRpcImport = true;
258
+ const [importName, serviceName] = ln.match(/\@\/services\/(.*)\.service/);
259
+ const funcsText = ln.replace(`from "${importName}"`, "").replace("import", "").replace("{", "").replace("}", "").replace(";", "");
260
+ const funcsName = funcsText.trim().split(",");
261
+ funcsName.forEach((fnName) => {
262
+ lines.push(`const ${fnName} = rpc("${serviceName}/${fnName}")`);
263
+ })
264
+ }
265
+ })
266
+ if (addRpcImport) {
267
+ lines.unshift(`import rpc from "parotta/rpc"`);
268
+ }
269
+ // remove .css and .service imports
258
- const filteredJsx = result.split("\n").filter((ln) => !ln.includes(".css")).join("\n");
270
+ const filteredJsx = lines.filter((ln) => !ln.includes(".css") && !ln.includes(".service")).join("\n");
259
271
  //.replaceAll("$jsx", "React.createElement");
260
272
  return new Response(filteredJsx, {
261
273
  headers: {
@@ -310,8 +322,8 @@ export default {
310
322
  if (match.page && req.headers.get("Accept")?.includes('text/html')) {
311
323
  return renderPage(url);
312
324
  }
313
- if (match.api) {
325
+ if (match.service) {
314
- return renderApi(`/routes${match.api}`, req);
326
+ return renderApi(match.key, match.service, req);
315
327
  }
316
328
  }
317
329
  if (req.headers.get("Accept")?.includes('text/html')) {
packages/parotta/router.js CHANGED
@@ -14,7 +14,7 @@ const getMatch = (radixRouter, pathname) => {
14
14
  return matchedPage;
15
15
  }
16
16
 
17
- const getCssUrl = (pathname) => `/routes${pathname === "/" ? "" : pathname}`;
17
+ const getCssUrl = (pathname) => `/pages${pathname === "/" ? "" : pathname}`;
18
18
 
19
19
  export const HeadApp = ({ history, radixRouter, importMap }) => {
20
20
  const pathname = useSyncExternalStore(history.listen, (v) => v ? v.location.pathname : history.location.pathname, () => history.location.pathname);
packages/parotta/rpc.js ADDED
@@ -0,0 +1,56 @@
1
+ import { useState, useMemo } from "react";
2
+
3
+ export const domain = () => typeof window !== 'undefined' ? window.origin : "http://0.0.0.0:3000";
4
+ export const globalCache = new Map();
5
+
6
+ const rpc = (serviceName) => async (params = {}) => {
7
+ const res = await fetch(`${domain()}/services/${serviceName}`, {
8
+ method: "POST",
9
+ headers: {
10
+ "Accept": "application/json",
11
+ "Content-Type": "application/json",
12
+ },
13
+ body: JSON.stringify(params),
14
+ })
15
+ return await res.json();
16
+ }
17
+
18
+ export const useCache = () => {
19
+ const [_, rerender] = useState(false);
20
+ const cache = useMemo(() => globalCache, []);
21
+ const get = (k) => cache.get(k)
22
+ const set = (k, v) => {
23
+ cache.set(k, v);
24
+ rerender((c) => !c);
25
+ }
26
+ const invalidate = (regex) => {
27
+ Array.from(cache.keys())
28
+ .filter((k) => regex.test(k))
29
+ .forEach((k) => {
30
+ fetchData(k).then((v) => set(k, v));
31
+ });
32
+ }
33
+ return {
34
+ get,
35
+ set,
36
+ invalidate,
37
+ }
38
+ }
39
+
40
+ export const useRpc = (fn, params) => {
41
+ const cache = useCache();
42
+ const key = `${fn.name}:${JSON.stringify(params)}`;
43
+ const value = cache.get(key);
44
+ if (value) {
45
+ if (value instanceof Promise) {
46
+ throw value;
47
+ } else if (value instanceof Error) {
48
+ throw value;
49
+ }
50
+ return { data: value, cache };
51
+ }
52
+ cache.set(key, fn(params).then((v) => cache.set(key, v)));
53
+ throw cache.get(key);
54
+ }
55
+
56
+ export default rpc;