~repos /edge-city
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
377409cc
—
Peter John 2 years ago
implement fetch
- packages/example/package.json +1 -2
- packages/example/routes/404/page.jsx +2 -4
- packages/example/routes/about/page.jsx +10 -4
- packages/example/routes/app.jsx +7 -18
- packages/example/routes/page.jsx +20 -8
- packages/example/routes/todos/:id/api.js +8 -0
- packages/example/routes/todos/:id/page.css +11 -0
- packages/example/routes/todos/:id/page.jsx +14 -0
- packages/example/routes/todos/page.jsx +5 -8
- packages/parotta/cli.js +2 -2
- packages/parotta/router.js +71 -21
- readme.md +5 -6
packages/example/package.json
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"react": "18.2.0",
|
|
12
|
-
"react-dom": "^18.2.0"
|
|
12
|
+
"react-dom": "^18.2.0"
|
|
13
|
-
"swr": "2.1.0"
|
|
14
13
|
}
|
|
15
14
|
}
|
packages/example/routes/404/page.jsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import "./page.css";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
export default function Page() {
|
|
5
5
|
return (
|
|
6
6
|
<div className="notfound-page">
|
|
7
7
|
<div>
|
|
@@ -12,6 +12,4 @@ const NotFound = () => {
|
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
14
14
|
)
|
|
15
|
-
}
|
|
15
|
+
}
|
|
16
|
-
|
|
17
|
-
export default NotFound;
|
packages/example/routes/about/page.jsx
CHANGED
|
@@ -2,7 +2,15 @@ import React from 'react';
|
|
|
2
2
|
import { Link, useRouter } from "parotta/router";
|
|
3
3
|
import "./page.css";
|
|
4
4
|
|
|
5
|
+
export function Head() {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
<title>About us</title>
|
|
9
|
+
</>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
5
|
-
|
|
13
|
+
export default function Page() {
|
|
6
14
|
const router = useRouter();
|
|
7
15
|
return (
|
|
8
16
|
<div className="about-page">
|
|
@@ -17,6 +25,4 @@ const AboutPage = () => {
|
|
|
17
25
|
</footer>
|
|
18
26
|
</div>
|
|
19
27
|
)
|
|
20
|
-
}
|
|
28
|
+
}
|
|
21
|
-
|
|
22
|
-
export default AboutPage;
|
packages/example/routes/app.jsx
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
import React, { Suspense } from "react";
|
|
2
|
-
import { SWRConfig } from "swr";
|
|
3
2
|
import { ErrorBoundary } from "parotta/error";
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
export default function App({ children }) {
|
|
6
5
|
console.log('app');
|
|
7
6
|
return (
|
|
8
|
-
<SWRConfig value={{
|
|
9
|
-
fallback: {
|
|
10
|
-
'https://jsonplaceholder.typicode.com/todos/1': { id: '123' },
|
|
11
|
-
},
|
|
12
|
-
// fallbackData: null,
|
|
13
|
-
fetcher: (resource, init) => fetch(resource, init).then(res => res.json()), suspense: true
|
|
14
|
-
}}>
|
|
15
|
-
|
|
7
|
+
<ErrorBoundary onError={(err) => console.log(err)} fallback={<p>Oops something went wrong</p>}>
|
|
16
|
-
|
|
8
|
+
<Suspense fallback={<p>Loading...</p>}>
|
|
17
|
-
|
|
9
|
+
{children}
|
|
18
|
-
|
|
10
|
+
</Suspense>
|
|
19
|
-
|
|
11
|
+
</ErrorBoundary>
|
|
20
|
-
</SWRConfig>
|
|
21
12
|
)
|
|
22
|
-
}
|
|
13
|
+
}
|
|
23
|
-
|
|
24
|
-
export default App;
|
packages/example/routes/page.jsx
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
-
import useSWR from "swr";
|
|
3
|
-
import { Link, useRouter } from "parotta/router";
|
|
2
|
+
import { Link, useRouter, useFetch } from "parotta/router";
|
|
4
3
|
import Counter from "@/components/Counter/Counter";
|
|
5
4
|
import "./page.css";
|
|
6
5
|
|
|
6
|
+
export function Head() {
|
|
7
|
+
const { data } = useFetch("/todos");
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<title>Parotta</title>
|
|
11
|
+
</>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
7
|
-
|
|
15
|
+
export default function Page() {
|
|
16
|
+
const { data, cache } = useFetch("/todos");
|
|
8
17
|
console.log('page');
|
|
18
|
+
console.log('data', data);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
cache.invalidate(/todos/);
|
|
22
|
+
}, 3000)
|
|
23
|
+
}, [])
|
|
9
24
|
// const todo = useAsync('123', () => getData());
|
|
10
|
-
const { data } = useSWR(`https://jsonplaceholder.typicode.com/todos/1`);
|
|
11
25
|
const router = useRouter();
|
|
12
26
|
return (
|
|
13
27
|
<div className="home-page">
|
|
@@ -23,6 +37,4 @@ const HomePage = () => {
|
|
|
23
37
|
</footer>
|
|
24
38
|
</div>
|
|
25
39
|
)
|
|
26
|
-
}
|
|
40
|
+
}
|
|
27
|
-
|
|
28
|
-
export default HomePage;
|
packages/example/routes/todos/:id/api.js
CHANGED
|
@@ -11,6 +11,14 @@ export const onGet = async (req) => {
|
|
|
11
11
|
const item = await todosCollection.findOne({
|
|
12
12
|
filter: { id },
|
|
13
13
|
});
|
|
14
|
+
if (!item) {
|
|
15
|
+
return new Response(`todo '${id}' not found`, {
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
},
|
|
19
|
+
status: 404,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
14
22
|
const data = JSON.stringify(item);
|
|
15
23
|
return new Response(data, {
|
|
16
24
|
headers: {
|
packages/example/routes/todos/:id/page.css
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
.home-page {
|
|
2
|
+
padding: 10px;
|
|
3
|
+
background-color: turquoise;
|
|
4
|
+
|
|
5
|
+
& .count {
|
|
6
|
+
color: black;
|
|
7
|
+
padding: 40px;
|
|
8
|
+
font-size: 30px;
|
|
9
|
+
font-weight: 600;
|
|
10
|
+
}
|
|
11
|
+
}
|
packages/example/routes/todos/:id/page.jsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
import { Link, useRouter, useFetch } from "parotta/router";
|
|
3
|
+
// import "./index.css";
|
|
4
|
+
|
|
5
|
+
export default function Page() {
|
|
6
|
+
const { params } = useRouter();
|
|
7
|
+
const data = useFetch(`/todos/${params.id}`);
|
|
8
|
+
console.log('data', data);
|
|
9
|
+
return (
|
|
10
|
+
<div className="todos-page">
|
|
11
|
+
<h1>Todo no: {params.id}</h1>
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
packages/example/routes/todos/page.jsx
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { Suspense } from "react";
|
|
2
|
-
import { useRouter } from "muffinjs/router.js";
|
|
3
|
-
import TodoList from "@/containers/TodoList/TodoList
|
|
2
|
+
import TodoList from "@/containers/TodoList/TodoList";
|
|
4
3
|
// import "./index.css";
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
export default function Page() {
|
|
7
6
|
return (
|
|
8
7
|
<div className="todos-page">
|
|
9
8
|
<h1>Todos</h1>
|
|
10
|
-
<Suspense>
|
|
9
|
+
{/* <Suspense>
|
|
11
10
|
<TodoList todos={todos} />
|
|
12
|
-
</Suspense>
|
|
11
|
+
</Suspense> */}
|
|
13
12
|
</div>
|
|
14
13
|
)
|
|
15
|
-
}
|
|
14
|
+
}
|
|
16
|
-
|
|
17
|
-
export default TodosPage;
|
packages/parotta/cli.js
CHANGED
|
@@ -112,10 +112,10 @@ const renderPage = async (filePath, url, params) => {
|
|
|
112
112
|
const components = mapDeps("components");
|
|
113
113
|
const containers = mapDeps("containers");
|
|
114
114
|
const cssFile = `${filePath.replace("jsx", "css")}`;
|
|
115
|
+
const mainPage = await import(path.join(process.cwd(), filePath));
|
|
115
116
|
const stream = await renderToReadableStream(
|
|
116
117
|
<html lang="en">
|
|
117
118
|
<head>
|
|
118
|
-
<title>Parotta</title>
|
|
119
119
|
<link rel="preload" href={cssFile} as="style" />
|
|
120
120
|
<link rel="stylesheet" href={cssFile} />
|
|
121
121
|
<script type="importmap" dangerouslySetInnerHTML={{
|
|
@@ -140,6 +140,7 @@ const renderPage = async (filePath, url, params) => {
|
|
|
140
140
|
)
|
|
141
141
|
}}>
|
|
142
142
|
</script>
|
|
143
|
+
<mainPage.Head />
|
|
143
144
|
<script type="module" defer={true} dangerouslySetInnerHTML={{
|
|
144
145
|
__html: `
|
|
145
146
|
import React from "react";
|
|
@@ -180,7 +181,6 @@ hydrateRoot(document.getElementById("root"), React.createElement(Router, {
|
|
|
180
181
|
</body>
|
|
181
182
|
</html >
|
|
182
183
|
);
|
|
183
|
-
// injectToStream('<script type="module" src="/main.js"></script>', { flush: true });
|
|
184
184
|
return new Response(stream, {
|
|
185
185
|
headers: { 'Content-Type': 'text/html' },
|
|
186
186
|
status: 200,
|
packages/parotta/router.js
CHANGED
|
@@ -1,8 +1,63 @@
|
|
|
1
|
-
import React, { createContext, useContext, useState, useEffect } from "react";
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
export const isClient = () => typeof window !== 'undefined';
|
|
4
|
+
export const domain = () => isClient() ? window.origin : "http://0.0.0.0:3000";
|
|
5
|
+
export const globalCache = new Map();
|
|
6
|
+
export const useFetchCache = () => {
|
|
7
|
+
const [_, rerender] = useState(false);
|
|
8
|
+
const cache = useMemo(() => globalCache, []);
|
|
9
|
+
const get = (k) => cache.get(k)
|
|
10
|
+
const set = (k, v) => {
|
|
11
|
+
cache.set(k, v);
|
|
12
|
+
rerender((c) => !c);
|
|
13
|
+
}
|
|
14
|
+
const invalidate = (regex) => {
|
|
15
|
+
Array.from(cache.keys())
|
|
16
|
+
.filter((k) => regex.test(k))
|
|
17
|
+
.forEach((k) => {
|
|
18
|
+
fetchData(k).then((v) => set(k, v));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
get,
|
|
23
|
+
set,
|
|
24
|
+
invalidate,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const fetchData = async (route) => {
|
|
29
|
+
const url = `${domain()}${route}`;
|
|
30
|
+
console.log('url', url);
|
|
31
|
+
const res = await fetch(url, {
|
|
32
|
+
headers: {
|
|
33
|
+
"Accept": "application/json",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (res.ok) {
|
|
37
|
+
return await res.json();
|
|
38
|
+
} else {
|
|
39
|
+
return new Error(await res.text());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const useFetch = (url) => {
|
|
44
|
+
const cache = useFetchCache();
|
|
45
|
+
const value = cache.get(url);
|
|
46
|
+
if (value) {
|
|
47
|
+
if (value instanceof Promise) {
|
|
48
|
+
throw value;
|
|
49
|
+
} else if (value instanceof Error) {
|
|
50
|
+
throw value;
|
|
51
|
+
}
|
|
52
|
+
return { data: value, cache };
|
|
53
|
+
}
|
|
54
|
+
cache.set(url, fetchData(url).then((v) => cache.set(url, v)));
|
|
55
|
+
throw cache.get(url);
|
|
56
|
+
}
|
|
2
57
|
|
|
3
58
|
export const RouterContext = createContext(undefined);
|
|
4
59
|
|
|
5
|
-
const
|
|
60
|
+
const getMatch = (radixRouter, pathname) => {
|
|
6
61
|
const matchedPage = radixRouter.lookup(pathname);
|
|
7
62
|
if (!matchedPage) {
|
|
8
63
|
return radixRouter.lookup("/404");
|
|
@@ -24,40 +79,35 @@ const loadCss = (pathname) => {
|
|
|
24
79
|
}
|
|
25
80
|
|
|
26
81
|
export const Router = ({ App, history, radixRouter }) => {
|
|
27
|
-
const [
|
|
82
|
+
const [MatchedPage, setMatchedPage] = useState(() => getMatch(radixRouter, history.location.pathname));
|
|
28
83
|
useEffect(() => {
|
|
29
84
|
return history.listen(({ location }) => {
|
|
30
85
|
loadCss(location.pathname);
|
|
31
|
-
|
|
86
|
+
setMatchedPage(getMatch(radixRouter, location.pathname));
|
|
32
87
|
});
|
|
33
88
|
}, [])
|
|
34
89
|
console.log('Router');
|
|
35
90
|
return React.createElement(RouterContext.Provider, {
|
|
91
|
+
value: {
|
|
36
|
-
|
|
92
|
+
history: history,
|
|
93
|
+
params: MatchedPage.params || {},
|
|
94
|
+
},
|
|
37
95
|
children: React.createElement(App, {
|
|
38
|
-
children: React.createElement(
|
|
96
|
+
children: React.createElement(MatchedPage, {})
|
|
39
97
|
}),
|
|
40
98
|
});
|
|
41
99
|
}
|
|
42
100
|
|
|
43
101
|
export const useRouter = () => {
|
|
44
|
-
const history = useContext(RouterContext);
|
|
102
|
+
const { history, params } = useContext(RouterContext);
|
|
45
103
|
return {
|
|
46
104
|
pathname: history.location.pathname,
|
|
47
|
-
query:
|
|
105
|
+
query: new URLSearchParams(history.location.search),
|
|
48
|
-
params
|
|
106
|
+
params,
|
|
49
|
-
push: (path) => {
|
|
50
|
-
|
|
107
|
+
push: history.push,
|
|
51
|
-
},
|
|
52
|
-
replace:
|
|
108
|
+
replace: history.replace,
|
|
53
|
-
history.replace(path)
|
|
54
|
-
},
|
|
55
|
-
forward: () => {
|
|
56
|
-
|
|
109
|
+
forward: history.forward,
|
|
57
|
-
},
|
|
58
|
-
back: () => {
|
|
59
|
-
|
|
110
|
+
back: history.back,
|
|
60
|
-
},
|
|
61
111
|
reload: () => window.location.reload(),
|
|
62
112
|
};
|
|
63
113
|
}
|
readme.md
CHANGED
|
@@ -5,9 +5,8 @@ It uses File system routing with SSR with streaming + CSR as the method to rende
|
|
|
5
5
|
It is very opionated and has set of idiomatic ways of doing things.
|
|
6
6
|
|
|
7
7
|
### Todo
|
|
8
|
-
1.
|
|
8
|
+
1. Hydrate fetch cache
|
|
9
|
-
2. Add helmet functionality
|
|
10
|
-
|
|
9
|
+
2. Improve css loading
|
|
11
|
-
|
|
10
|
+
3. Improve react page loading (try to remove flash)
|
|
12
|
-
|
|
11
|
+
4. Add build step
|
|
13
|
-
|
|
12
|
+
5. Deploy to Docker, Deno deploy, Cloudflare workers, Vercel edge functions, Bun edge (whenever it releases)
|