~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


b76450ae Peter John

2 years ago
support api rendering
packages/example/routes/todos/:id/api.js CHANGED
@@ -1,9 +1,13 @@
1
- import tigrisDB from "./db.js.js.js.js.js.js.js.js";
1
+ import { todosCollection } from "@/services/collections";
2
2
 
3
+ const getId = (req) => {
4
+ const url = new URL(req.url);
3
- export const todosCollection = tigrisDB.getCollection("todoItems");
5
+ const res = new RegExp("/todos/(.*?)$").exec(url.pathname)
6
+ return res[1];
7
+ }
4
8
 
5
9
  export const onGet = async (req) => {
6
- const { id } = req.params;
10
+ const id = getId(req);
7
11
  const item = await todosCollection.findOne({
8
12
  filter: { id },
9
13
  });
@@ -19,7 +23,7 @@ export const onGet = async (req) => {
19
23
  export const onPut = async (req) => {
20
24
  const item = await req.body();
21
25
  const updated = await todosCollection.insertOrReplaceOne(item);
22
- const data = JSON.stringify(updated);``
26
+ const data = JSON.stringify(updated);
23
27
  return new Response(data, {
24
28
  headers: {
25
29
  "Content-Type": "application/json",
@@ -29,7 +33,7 @@ export const onPut = async (req) => {
29
33
  }
30
34
 
31
35
  export const onDelete = async (req) => {
32
- const { id } = req.params;
36
+ const id = getId(req);
33
37
  const res = await todosCollection.deleteOne({
34
38
  filter: { id },
35
39
  });
packages/example/routes/todos/api.js CHANGED
@@ -1,6 +1,4 @@
1
- import tigrisDB from "./db.js.js.js";
2
-
3
- export const todosCollection = tigrisDB.getCollection("todoItems");
1
+ import { todosCollection } from "@/services/collections";
4
2
 
5
3
  export const onGet = async (req) => {
6
4
  const cursor = todosCollection.findMany({});
packages/example/services/collections.js ADDED
@@ -0,0 +1,34 @@
1
+ const getCollection = (name) => {
2
+ let items = [];
3
+ return {
4
+ findMany: () => {
5
+ return {
6
+ toArray: async () => {
7
+ return items;
8
+ }
9
+ }
10
+ },
11
+ findOne: ({ filter }) => {
12
+ return items.find((item) => item.id === filter.id)
13
+ },
14
+ insertOne: (item) => {
15
+ items.push(item);
16
+ return item;
17
+ },
18
+ insertOrReplaceOne: (item) => {
19
+ const index = items.findIndex((it) => it.id === item.id);
20
+ if (index) {
21
+ items[index] = item;
22
+ } else {
23
+ items.push(item);
24
+ }
25
+ return item;
26
+ },
27
+ deleteOne: ({ filter }) => {
28
+ items = items.filter((item) => item.id !== filter.id);
29
+ return { id: filter.id }
30
+ },
31
+ }
32
+ }
33
+
34
+ export const todosCollection = getCollection("todos");
packages/muffinjs/bun.lockb CHANGED
Binary file
packages/muffinjs/index.js CHANGED
@@ -63,14 +63,47 @@ const mapDeps = (dir) => {
63
63
  }, {});
64
64
  }
65
65
 
66
+ const hydrateScript = (filePath, initialRouteValue) => {
67
+ return `
68
+ import React from 'react';
69
+ import { hydrateRoot } from 'react-dom/client';
70
+ // import { routerAtom } from "muffinjs/router";
71
+ import Page from "${filePath}";
72
+
73
+ // routerAtom.update(() => (${JSON.stringify(initialRouteValue)}));
74
+
75
+ hydrateRoot(document.getElementById("root"), React.createElement(Page, {}, undefined, false, undefined, this));
76
+ `;
77
+ }
78
+
66
79
  const radixRouter = createRouter({
67
80
  strictTrailingSlash: true,
68
81
  routes: mapFiles(),
69
82
  });
70
83
 
71
84
  const renderApi = async (filePath, req) => {
72
- const routeImport = await import(route.filePath);
85
+ const routeImport = await import(path.join(process.cwd(), filePath));
86
+ switch (req.method) {
87
+ case "HEAD":
88
+ return routeImport.onHead(req);
89
+ case "OPTIONS":
90
+ return routeImport.onOptions(req);
91
+ case "GET":
92
+ return routeImport.onGet(req);
93
+ case "POST":
94
+ return routeImport.onPost(req);
95
+ case "PUT":
96
+ return routeImport.onPut(req);
97
+ case "PATCH":
98
+ return routeImport.onPatch(req);
99
+ case "DELETE":
73
- console.log('routeImport', routeImport);
100
+ return routeImport.onDelete(req);
101
+ default:
102
+ return new Response(`{"message": "route not found"}`, {
103
+ headers: { 'Content-Type': 'application/json' },
104
+ status: 404,
105
+ });
106
+ }
74
107
  }
75
108
 
76
109
  const renderPage = async (filePath, url, params) => {
@@ -86,29 +119,30 @@ const renderPage = async (filePath, url, params) => {
86
119
  routerAtom.update(() => initialRouteValue);
87
120
  const routeImport = await import(path.join(process.cwd(), filePath));
88
121
  const packageJson = await import(path.join(process.cwd(), "package.json"));
89
- const dependencies = packageJson.default.dependencies;
90
122
  const devTag = !isProd ? "?dev" : "";
91
- const imports = Object.keys(dependencies).reduce((acc, dep) => {
123
+ const nodeDeps = Object.keys(packageJson.default.dependencies).reduce((acc, dep) => {
92
- acc[dep] = `https://esm.sh/${dep}@${dependencies[dep]}${devTag}`;
124
+ acc[dep] = `https://esm.sh/${dep}@${packageJson.default.dependencies[dep]}${devTag}`;
93
125
  return acc;
94
126
  }, {})
95
127
  const Page = routeImport.default;
96
128
  const components = mapDeps("components");
97
129
  const containers = mapDeps("containers");
98
- const parottaVersion = dependencies["parotta"];
130
+ const parottaVersion = packageJson.default.dependencies["parotta"];
131
+ const cssFile = `${filePath.replace("jsx", "css")}`;
99
132
  const stream = await renderToReadableStream(
100
133
  <html lang="en">
101
134
  <head>
102
- <link rel="stylesheet" href={`${filePath.replace("jsx", "css")}`} />
135
+ <link rel="preload" href={cssFile} as="style" />
136
+ <link rel="stylesheet" href={cssFile} />
103
137
  <script type="importmap" dangerouslySetInnerHTML={{
104
138
  __html: JSON.stringify(
105
139
  {
106
140
  "imports": {
107
- ...imports,
108
141
  "radix3": `https://esm.sh/radix3`,
109
142
  "react-dom/client": `https://esm.sh/react-dom@18.2.0/client${devTag}`,
110
143
  "react/jsx-dev-runtime": `https://esm.sh/react@18.2.0/jsx-dev-runtime${devTag}`,
111
144
  "parotta/router": `https://esm.sh/parotta@${parottaVersion}`,
145
+ ...nodeDeps,
112
146
  ...components,
113
147
  ...containers,
114
148
  }
@@ -117,16 +151,11 @@ const renderPage = async (filePath, url, params) => {
117
151
  }}>
118
152
  </script>
119
153
  <script type="module" defer dangerouslySetInnerHTML={{
120
- __html: `
121
- import React from 'react';
122
- import { hydrateRoot } from 'react-dom/client';
123
- // import { routerAtom } from "muffinjs/router";
124
- import Page from "${filePath}";
125
-
126
- // routerAtom.update(() => (${JSON.stringify(initialRouteValue)}));
154
+ __html: hydrateScript(filePath, initialRouteValue)
127
-
128
- hydrateRoot(document.getElementById("root"), React.createElement(Page, {}, undefined, false, undefined, this));
129
- `}}></script>
155
+ }}></script>
156
+ <title>
157
+ Parotta
158
+ </title>
130
159
  </head>
131
160
  <body>
132
161
  <div id="root">
@@ -210,7 +239,7 @@ export default {
210
239
  if (match.file) {
211
240
  return sendFile(`/static${match.file}`);
212
241
  }
213
- if (match.page) {
242
+ if (match.page && req.headers.get("Accept")?.includes('text/html')) {
214
243
  return renderPage(`/routes${match.page}`, url, match.params);
215
244
  }
216
245
  if (match.api) {
@@ -222,4 +251,11 @@ export default {
222
251
  status: 404,
223
252
  });
224
253
  },
254
+ // error(error) {
255
+ // return new Response(`<pre>${error}\n${error.stack}</pre>`, {
256
+ // headers: {
257
+ // "Content-Type": "text/html",
258
+ // },
259
+ // });
260
+ // },
225
- };
261
+ }
packages/muffinjs/package.json CHANGED
@@ -12,13 +12,13 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "autoprefixer": "^10.4.14",
15
+ "mime-types": "2.1.35",
15
16
  "postcss": "^8.4.21",
16
17
  "postcss-custom-media": "^9.1.2",
17
18
  "postcss-nesting": "^11.2.1",
18
19
  "postcss-normalize": "^10.0.1",
19
20
  "radix3": "^1.0.0",
20
- "walkdir": "0.4.1",
21
+ "walkdir": "0.4.1"
21
- "mime-types": "2.1.35"
22
22
  },
23
23
  "bin": {
24
24
  "muffin": "./bin/muffin"