~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
091ac2e7
—
Peter John 2 years ago
improve routing
- packages/example/bun.lockb +0 -0
- packages/example/routes/about/page.jsx +4 -1
- packages/example/routes/app.jsx +3 -5
- packages/example/routes/page.jsx +1 -0
- packages/parotta/bun.lockb +0 -0
- packages/parotta/cli.js +62 -64
- packages/parotta/package.json +1 -0
- packages/parotta/router.js +29 -55
- readme.md +5 -0
packages/example/bun.lockb
CHANGED
|
Binary file
|
packages/example/routes/about/page.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { useRouter } from "parotta/router";
|
|
2
|
+
import { Link, useRouter } from "parotta/router";
|
|
3
3
|
import "./page.css";
|
|
4
4
|
|
|
5
5
|
const AboutPage = () => {
|
|
@@ -12,6 +12,9 @@ const AboutPage = () => {
|
|
|
12
12
|
Path: {router.pathname}
|
|
13
13
|
</p>
|
|
14
14
|
</div>
|
|
15
|
+
<footer>
|
|
16
|
+
<Link href="/">Back</Link>
|
|
17
|
+
</footer>
|
|
15
18
|
</div>
|
|
16
19
|
)
|
|
17
20
|
}
|
packages/example/routes/app.jsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React, { Suspense } from "react";
|
|
2
2
|
import { SWRConfig } from "swr";
|
|
3
|
-
import { RouterProvider } from "parotta/router";
|
|
4
3
|
import { ErrorBoundary } from "parotta/error";
|
|
5
4
|
|
|
6
|
-
const App = ({
|
|
5
|
+
const App = ({ children }) => {
|
|
6
|
+
console.log('app');
|
|
7
7
|
return (
|
|
8
8
|
<SWRConfig value={{
|
|
9
9
|
fallback: {
|
|
@@ -14,9 +14,7 @@ const App = ({ routerProps, children }) => {
|
|
|
14
14
|
}}>
|
|
15
15
|
<ErrorBoundary onError={(err) => console.log(err)} fallback={<p>Oops something went wrong</p>}>
|
|
16
16
|
<Suspense fallback={<p>Loading...</p>}>
|
|
17
|
-
<RouterProvider value={routerProps}>
|
|
18
|
-
|
|
17
|
+
{children}
|
|
19
|
-
</RouterProvider>
|
|
20
18
|
</Suspense>
|
|
21
19
|
</ErrorBoundary>
|
|
22
20
|
</SWRConfig>
|
packages/example/routes/page.jsx
CHANGED
|
@@ -5,6 +5,7 @@ import Counter from "@/components/Counter/Counter";
|
|
|
5
5
|
import "./page.css";
|
|
6
6
|
|
|
7
7
|
const HomePage = () => {
|
|
8
|
+
console.log('page');
|
|
8
9
|
// const todo = useAsync('123', () => getData());
|
|
9
10
|
const { data } = useSWR(`https://jsonplaceholder.typicode.com/todos/1`);
|
|
10
11
|
const router = useRouter();
|
packages/parotta/bun.lockb
CHANGED
|
Binary file
|
packages/parotta/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun --hot
|
|
2
2
|
|
|
3
|
+
import React from 'react';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import walkdir from 'walkdir';
|
|
5
6
|
import postcss from "postcss"
|
|
@@ -7,8 +8,10 @@ import autoprefixer from "autoprefixer";
|
|
|
7
8
|
import postcssCustomMedia from "postcss-custom-media";
|
|
8
9
|
// import postcssNormalize from 'postcss-normalize';
|
|
9
10
|
import postcssNesting from "postcss-nesting";
|
|
11
|
+
import { createMemoryHistory } from "history";
|
|
10
12
|
import { createRouter } from 'radix3';
|
|
11
13
|
import mimeTypes from "mime-types";
|
|
14
|
+
import { Router } from "./router";
|
|
12
15
|
import { renderToReadableStream } from 'react-dom/server';
|
|
13
16
|
// import { renderToStream } from './render';
|
|
14
17
|
|
|
@@ -20,7 +23,7 @@ console.log("running in folder:", path.basename(process.cwd()), "env:", process.
|
|
|
20
23
|
|
|
21
24
|
const isProd = process.env.NODE_ENV === "production";
|
|
22
25
|
|
|
23
|
-
const mapFiles = (
|
|
26
|
+
const mapFiles = () => {
|
|
24
27
|
const routes = {};
|
|
25
28
|
const dirs = walkdir.sync(path.join(process.cwd(), "routes"))
|
|
26
29
|
.map((s) => s.replace(process.cwd(), "")
|
|
@@ -34,23 +37,19 @@ const mapFiles = ({ includeStatic, includeApi }) => {
|
|
|
34
37
|
const key = page.route || "/";
|
|
35
38
|
routes[key] = { key: key, page: page.path };
|
|
36
39
|
});
|
|
37
|
-
if (includeApi) {
|
|
38
|
-
|
|
40
|
+
dirs.filter((p) => p.includes('api.js'))
|
|
39
|
-
|
|
41
|
+
.map((s) => s.replace(process.cwd(), ""))
|
|
40
|
-
|
|
42
|
+
.map((s) => ({ path: s, route: s.replace("/api.js", "") }))
|
|
41
|
-
|
|
43
|
+
.forEach((api) => {
|
|
42
|
-
|
|
44
|
+
const key = api.route || "/";
|
|
43
|
-
|
|
45
|
+
routes[key] = routes[key] || { key };
|
|
44
|
-
|
|
46
|
+
routes[key].api = api.path;
|
|
45
|
-
|
|
47
|
+
});
|
|
46
|
-
}
|
|
47
|
-
if (includeStatic) {
|
|
48
|
-
|
|
48
|
+
walkdir.sync(path.join(process.cwd(), "static"))
|
|
49
|
-
|
|
49
|
+
.map((s) => s.replace(process.cwd(), "").replace("/static", ""))
|
|
50
|
-
|
|
50
|
+
.forEach((route) => {
|
|
51
|
-
|
|
51
|
+
routes[route] = { key: route, file: route }
|
|
52
|
-
|
|
52
|
+
});
|
|
53
|
-
}
|
|
54
53
|
return routes;
|
|
55
54
|
}
|
|
56
55
|
|
|
@@ -64,32 +63,20 @@ const mapDeps = (dir) => {
|
|
|
64
63
|
}, {});
|
|
65
64
|
}
|
|
66
65
|
|
|
66
|
+
const mapPages = () => walkdir.sync(path.join(process.cwd(), "routes"))
|
|
67
|
+
.filter((p) => p.includes('page.jsx'))
|
|
67
|
-
|
|
68
|
+
.map((s) => s.replace(process.cwd(), ""))
|
|
69
|
+
.map((s) => s.replace("/routes", ""))
|
|
70
|
+
.map((s) => s.replace("/page.jsx", ""));
|
|
71
|
+
|
|
68
|
-
const serverSideRoutes = mapFiles(
|
|
72
|
+
const serverSideRoutes = mapFiles();
|
|
69
|
-
const clientSideRoutes =
|
|
73
|
+
const clientSideRoutes = mapPages();
|
|
70
74
|
|
|
71
75
|
const radixRouter = createRouter({
|
|
72
76
|
strictTrailingSlash: true,
|
|
73
77
|
routes: serverSideRoutes,
|
|
74
78
|
});
|
|
75
79
|
|
|
76
|
-
const hydrateScript = (appPath, pagePath, routerProps) => {
|
|
77
|
-
return `
|
|
78
|
-
import React from "react";
|
|
79
|
-
import { hydrateRoot } from "react-dom/client";
|
|
80
|
-
import App from "${removeCwd(appPath)}";
|
|
81
|
-
import Page from "${removeCwd(pagePath)}";
|
|
82
|
-
|
|
83
|
-
const routerProps = ${JSON.stringify(routerProps)};
|
|
84
|
-
|
|
85
|
-
hydrateRoot(document.getElementById("root"), React.createElement(App, {
|
|
86
|
-
routerProps: routerProps,
|
|
87
|
-
children: React.createElement(Page, {}),
|
|
88
|
-
}));
|
|
89
|
-
`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
80
|
const renderApi = async (filePath, req) => {
|
|
94
81
|
const routeImport = await import(path.join(process.cwd(), filePath));
|
|
95
82
|
switch (req.method) {
|
|
@@ -116,33 +103,19 @@ const renderApi = async (filePath, req) => {
|
|
|
116
103
|
}
|
|
117
104
|
|
|
118
105
|
const renderPage = async (filePath, url, params) => {
|
|
119
|
-
const query = {};
|
|
120
|
-
for (const key of url.searchParams.keys()) {
|
|
121
|
-
query[key] = url.searchParams.get(key);
|
|
122
|
-
}
|
|
123
|
-
const routerProps = {
|
|
124
|
-
routes: clientSideRoutes,
|
|
125
|
-
state: {
|
|
126
|
-
query: query,
|
|
127
|
-
params: params,
|
|
128
|
-
pathname: url.pathname,
|
|
129
|
-
},
|
|
130
|
-
}
|
|
131
|
-
const appPath = path.join(process.cwd(), "routes", "app.jsx");
|
|
132
|
-
const pagePath = path.join(process.cwd(), filePath);
|
|
133
106
|
const packageJson = await import(path.join(process.cwd(), "package.json"));
|
|
134
107
|
const devTag = !isProd ? "?dev" : "";
|
|
135
108
|
const nodeDeps = Object.keys(packageJson.default.dependencies).reduce((acc, dep) => {
|
|
136
109
|
acc[dep] = `https://esm.sh/${dep}@${packageJson.default.dependencies[dep]}`;
|
|
137
110
|
return acc;
|
|
138
111
|
}, {})
|
|
139
|
-
const App = (await import(appPath)).default;
|
|
140
112
|
const components = mapDeps("components");
|
|
141
113
|
const containers = mapDeps("containers");
|
|
142
114
|
const cssFile = `${filePath.replace("jsx", "css")}`;
|
|
143
115
|
const stream = await renderToReadableStream(
|
|
144
116
|
<html lang="en">
|
|
145
117
|
<head>
|
|
118
|
+
<title>Parotta</title>
|
|
146
119
|
<link rel="preload" href={cssFile} as="style" />
|
|
147
120
|
<link rel="stylesheet" href={cssFile} />
|
|
148
121
|
<script type="importmap" dangerouslySetInnerHTML={{
|
|
@@ -150,11 +123,11 @@ const renderPage = async (filePath, url, params) => {
|
|
|
150
123
|
{
|
|
151
124
|
"imports": {
|
|
152
125
|
"radix3": `https://esm.sh/radix3`,
|
|
126
|
+
"history": "https://esm.sh/history@5.3.0",
|
|
153
|
-
"react": `https://esm.sh/react@18.2.0`,
|
|
127
|
+
"react": `https://esm.sh/react@18.2.0${devTag}`,
|
|
154
|
-
// TODO: need to remove this
|
|
128
|
+
// TODO: need to remove this in prod
|
|
155
|
-
"react/jsx-dev-runtime": `https://esm.sh/react@18.2.0/jsx-dev-runtime`,
|
|
129
|
+
"react/jsx-dev-runtime": `https://esm.sh/react@18.2.0${devTag}/jsx-dev-runtime`,
|
|
156
|
-
"react-dom/client": `https://esm.sh/react-dom@18.2.0/client`,
|
|
130
|
+
"react-dom/client": `https://esm.sh/react-dom@18.2.0${devTag}/client`,
|
|
157
|
-
"react-streaming": "https://esm.sh/react-streaming",
|
|
158
131
|
// "parotta/router": `https://esm.sh/parotta@${version}/router.js`,
|
|
159
132
|
// "parotta/error": `https://esm.sh/parotta@${version}/error.js`,
|
|
160
133
|
"parotta/router": `/parotta/router.js`,
|
|
@@ -168,16 +141,41 @@ const renderPage = async (filePath, url, params) => {
|
|
|
168
141
|
}}>
|
|
169
142
|
</script>
|
|
170
143
|
<script type="module" defer={true} dangerouslySetInnerHTML={{
|
|
144
|
+
__html: `
|
|
145
|
+
import React from "react";
|
|
146
|
+
import { hydrateRoot } from "react-dom/client";
|
|
147
|
+
import { createBrowserHistory } from "history";
|
|
148
|
+
import { createRouter } from "radix3";
|
|
171
|
-
|
|
149
|
+
import { Router } from "parotta/router";
|
|
150
|
+
|
|
151
|
+
hydrateRoot(document.getElementById("root"), React.createElement(Router, {
|
|
152
|
+
App: React.lazy(() => import("/routes/app.jsx")),
|
|
153
|
+
history: createBrowserHistory(),
|
|
154
|
+
radixRouter: createRouter({
|
|
155
|
+
strictTrailingSlash: true,
|
|
156
|
+
routes: {
|
|
157
|
+
${clientSideRoutes.map((r) => `"${r === "" ? "/" : r}": React.lazy(() => import("/routes${r === "" ? "" : r}/page.jsx"))`).join(',\n ')}
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
}));
|
|
161
|
+
`
|
|
172
162
|
}}></script>
|
|
173
|
-
<title>
|
|
174
|
-
Parotta
|
|
175
|
-
</title>
|
|
176
163
|
</head>
|
|
177
164
|
<body>
|
|
178
165
|
<div id="root">
|
|
166
|
+
<Router
|
|
167
|
+
App={React.lazy(() => import(`${process.cwd()}/routes/app.jsx`))}
|
|
168
|
+
history={createMemoryHistory({
|
|
169
|
+
initialEntries: [url.pathname + url.search],
|
|
170
|
+
})}
|
|
179
|
-
|
|
171
|
+
radixRouter={createRouter({
|
|
172
|
+
strictTrailingSlash: true,
|
|
173
|
+
routes: clientSideRoutes.reduce((acc, r) => {
|
|
174
|
+
acc[r === "" ? "/" : r] = React.lazy(() => import(`${process.cwd()}/routes${r === "" ? "" : r}/page.jsx`));
|
|
175
|
+
return acc
|
|
176
|
+
}, {})
|
|
177
|
+
})}
|
|
180
|
-
|
|
178
|
+
/>
|
|
181
179
|
</div>
|
|
182
180
|
</body>
|
|
183
181
|
</html >
|
packages/parotta/package.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"autoprefixer": "^10.4.14",
|
|
7
|
+
"history": "^5.3.0",
|
|
7
8
|
"mime-types": "2.1.35",
|
|
8
9
|
"postcss": "^8.4.21",
|
|
9
10
|
"postcss-custom-media": "^9.1.2",
|
packages/parotta/router.js
CHANGED
|
@@ -1,80 +1,55 @@
|
|
|
1
|
-
import React, { createContext, useContext, useState,
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from "react";
|
|
2
|
-
import { createRouter } from 'radix3';
|
|
3
2
|
|
|
4
|
-
export const RouterContext = createContext(
|
|
3
|
+
export const RouterContext = createContext(undefined);
|
|
5
|
-
stack: [],
|
|
6
|
-
state: {
|
|
7
|
-
pathname: "",
|
|
8
|
-
query: {},
|
|
9
|
-
params: {},
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
const getBasePath = () => typeof window !== "undefined" ? "" : process.cwd();
|
|
14
4
|
|
|
15
|
-
export const
|
|
5
|
+
export const Router = ({ App, history, radixRouter }) => {
|
|
16
|
-
const [
|
|
6
|
+
const [Page, setPage] = useState(radixRouter.lookup(history.location.pathname));
|
|
17
|
-
const [state, setState] = useState(value.state);
|
|
18
|
-
const [Page, setPage] = useState();
|
|
19
|
-
const radixRouter = useMemo(() => createRouter({
|
|
20
|
-
strictTrailingSlash: true,
|
|
21
|
-
routes: value.routes,
|
|
22
|
-
}), [])
|
|
23
7
|
useEffect(() => {
|
|
24
|
-
|
|
8
|
+
return history.listen(({ action, location }) => {
|
|
25
|
-
setPath(location.pathname);
|
|
26
|
-
}, false);
|
|
27
|
-
}, [])
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (path !== value.state.pathname) {
|
|
30
|
-
|
|
9
|
+
const matchedPage = radixRouter.lookup(location.pathname);
|
|
31
|
-
if (!
|
|
10
|
+
if (!matchedPage) {
|
|
32
|
-
|
|
11
|
+
matchedPage = '/404.jsx';
|
|
33
12
|
console.log('route not matched');
|
|
13
|
+
setPage(() => <h1> Not found</h1>);
|
|
34
14
|
} else {
|
|
35
|
-
console.log('route match',
|
|
15
|
+
console.log('route match', matchedPage);
|
|
16
|
+
setPage(matchedPage);
|
|
36
17
|
}
|
|
37
|
-
import(`${getBasePath()}/routes${match.page}`)
|
|
38
|
-
.then((comp) => {
|
|
39
|
-
setState({ pathname: path, params: match.params, query: {} });
|
|
40
|
-
setPage(comp.default);
|
|
41
|
-
|
|
18
|
+
});
|
|
42
|
-
} else {
|
|
43
|
-
setState(value.state);
|
|
44
|
-
setPage(null);
|
|
45
|
-
}
|
|
46
|
-
}, [
|
|
19
|
+
}, [])
|
|
20
|
+
console.log('Router');
|
|
47
21
|
return React.createElement(RouterContext.Provider, {
|
|
48
|
-
value:
|
|
22
|
+
value: history,
|
|
49
|
-
stack: [],
|
|
50
|
-
state,
|
|
51
|
-
|
|
23
|
+
children: React.createElement(App, {
|
|
24
|
+
children: React.createElement(Page, {})
|
|
52
|
-
},
|
|
25
|
+
}),
|
|
53
|
-
children: Page || children,
|
|
54
26
|
});
|
|
55
27
|
}
|
|
56
28
|
|
|
57
29
|
export const useRouter = () => {
|
|
58
|
-
const
|
|
30
|
+
const history = useContext(RouterContext);
|
|
59
31
|
return {
|
|
32
|
+
pathname: history.location.pathname,
|
|
33
|
+
query: {},
|
|
60
|
-
|
|
34
|
+
params: {},
|
|
61
35
|
push: (path) => {
|
|
36
|
+
history.push(path)
|
|
62
37
|
},
|
|
63
38
|
replace: (path) => {
|
|
39
|
+
history.replace(path)
|
|
64
40
|
},
|
|
65
|
-
|
|
41
|
+
forward: () => {
|
|
66
|
-
},
|
|
67
|
-
|
|
42
|
+
history.forward();
|
|
68
43
|
},
|
|
69
44
|
back: () => {
|
|
45
|
+
history.back();
|
|
70
46
|
},
|
|
71
47
|
reload: () => window.location.reload(),
|
|
72
48
|
};
|
|
73
49
|
}
|
|
74
50
|
|
|
75
51
|
export const Link = (props) => {
|
|
76
|
-
const
|
|
52
|
+
const router = useRouter();
|
|
77
|
-
|
|
78
53
|
return React.createElement("a", {
|
|
79
54
|
...props,
|
|
80
55
|
onClick: (e) => {
|
|
@@ -82,8 +57,7 @@ export const Link = (props) => {
|
|
|
82
57
|
if (props && props.onClick) {
|
|
83
58
|
props.onClick(e);
|
|
84
59
|
}
|
|
85
|
-
history.pushState({}, "", props.href);
|
|
86
|
-
|
|
60
|
+
router.push(props.href);
|
|
87
61
|
},
|
|
88
62
|
})
|
|
89
63
|
}
|
readme.md
CHANGED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# parotta
|
|
2
|
+
|
|
3
|
+
parotta is a next level meta-framework for react that runs only on the bun js runtime.
|
|
4
|
+
It uses File system routing with SSR with streaming + CSR as the method to render pages. Basically a MPA + SPA Transitional App.
|
|
5
|
+
It is very opionated and has set of idiomatic ways of doing things.
|