~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
3acb86f1
—
Peter John 2 years ago
use head and body
- packages/create-parotta-app/index.js +31 -0
- packages/create-parotta-app/package.json +16 -0
- packages/example/containers/TodoList/TodoList.jsx +1 -1
- packages/example/jsconfig.json +1 -2
- packages/example/routes/404/page.css +1 -1
- packages/example/routes/404/page.jsx +11 -7
- packages/example/routes/about/page.css +6 -1
- packages/example/routes/about/page.jsx +2 -2
- packages/example/routes/page.css +0 -3
- packages/example/routes/page.jsx +3 -3
- packages/example/routes/todos/:id/page.css +1 -8
- packages/example/routes/todos/:id/page.jsx +9 -3
- packages/example/routes/todos/page.css +1 -8
- packages/example/routes/todos/page.jsx +8 -3
- packages/parotta/cli.js +14 -16
- packages/parotta/router.js +22 -21
- readme.md +3 -1
packages/create-parotta-app/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import gunzip from 'gunzip-file';
|
|
9
|
+
import packageJson from './package.json'
|
|
10
|
+
|
|
11
|
+
process.on('SIGINT', () => process.exit(0))
|
|
12
|
+
process.on('SIGTERM', () => process.exit(0))
|
|
13
|
+
|
|
14
|
+
let projectPath = "";
|
|
15
|
+
const program = new Command(packageJson.name)
|
|
16
|
+
.version(packageJson.version)
|
|
17
|
+
.arguments('<project-directory>')
|
|
18
|
+
.usage(`${chalk.green('<project-directory>')} [options]`)
|
|
19
|
+
.action((name) => {
|
|
20
|
+
projectPath = name
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// program.parse();
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
const tmpDir = os.tmpdir();
|
|
27
|
+
console.log('tmpDir', tmpDir);
|
|
28
|
+
const tmpFile = path.join(tmpDir, "main.zip");
|
|
29
|
+
const res = await fetch("https://github.com/pyrossh/parotta/archive/refs/heads/main.zip");
|
|
30
|
+
await Bun.write(tmpFile, res);
|
|
31
|
+
await gunzip(tmpFile, os.tmpdir());
|
packages/create-parotta-app/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-parotta-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"chalk": "^5.2.0",
|
|
7
|
+
"commander": "10.0.0",
|
|
8
|
+
"extract-files": "^13.0.0",
|
|
9
|
+
"gunzip-file": "^0.1.1",
|
|
10
|
+
"tar": "^6.1.13",
|
|
11
|
+
"unzip": "^0.1.11"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"parotta": "./index.js"
|
|
15
|
+
}
|
|
16
|
+
}
|
packages/example/containers/TodoList/TodoList.jsx
CHANGED
|
@@ -7,7 +7,7 @@ const todos = [
|
|
|
7
7
|
];
|
|
8
8
|
|
|
9
9
|
const TodoList = () => {
|
|
10
|
-
// const { data: todos } =
|
|
10
|
+
// const { data: todos } = useFetch("/todos");
|
|
11
11
|
return (
|
|
12
12
|
<div className="todo-list">
|
|
13
13
|
<h1>Todos</h1>
|
packages/example/jsconfig.json
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
},
|
|
8
8
|
// TODO these options are not supported
|
|
9
9
|
"jsx": "react",
|
|
10
|
-
"jsxFactory": "React.createElement"
|
|
10
|
+
"jsxFactory": "React.createElement"
|
|
11
|
-
"jsxImportSource": "react"
|
|
12
11
|
}
|
|
13
12
|
}
|
packages/example/routes/404/page.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
body {
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-direction: column;
|
|
4
4
|
align-items: center;
|
packages/example/routes/404/page.jsx
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import "./page.css";
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export const Head = () => {
|
|
5
5
|
return (
|
|
6
|
-
<
|
|
6
|
+
<title>Page not found</title>
|
|
7
|
+
)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Body = () => {
|
|
11
|
+
return (
|
|
7
|
-
|
|
12
|
+
<div>
|
|
8
|
-
|
|
13
|
+
<h1>404</h1>
|
|
9
|
-
|
|
14
|
+
<div className="content">
|
|
10
|
-
|
|
15
|
+
<h2>This page could not be found</h2>
|
|
11
|
-
</div>
|
|
12
16
|
</div>
|
|
13
17
|
</div>
|
|
14
18
|
)
|
packages/example/routes/about/page.css
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
2
3
|
padding: 20px;
|
|
3
4
|
padding-bottom: 130px;
|
|
4
5
|
background-color: violet;
|
|
6
|
+
|
|
7
|
+
& footer {
|
|
8
|
+
margin-top: 100px;
|
|
9
|
+
}
|
|
5
10
|
}
|
packages/example/routes/about/page.jsx
CHANGED
|
@@ -2,13 +2,13 @@ import React from 'react';
|
|
|
2
2
|
import { Link, useRouter } from "parotta/router";
|
|
3
3
|
import "./page.css";
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export const Head = () => {
|
|
6
6
|
return (
|
|
7
7
|
<title>About us</title>
|
|
8
8
|
)
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export
|
|
11
|
+
export const Body = () => {
|
|
12
12
|
const router = useRouter();
|
|
13
13
|
return (
|
|
14
14
|
<div className="about-page">
|
packages/example/routes/page.css
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
body {
|
|
2
2
|
margin: 0;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
.home-page {
|
|
6
3
|
padding: 20px;
|
|
7
4
|
margin: 0;
|
|
8
5
|
background-color: turquoise;
|
packages/example/routes/page.jsx
CHANGED
|
@@ -4,14 +4,14 @@ import { useFetch } from "parotta/fetch";
|
|
|
4
4
|
import Counter from "@/components/Counter/Counter";
|
|
5
5
|
import "./page.css";
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export const Head = () => {
|
|
8
8
|
const { data } = useFetch("/todos");
|
|
9
9
|
return (
|
|
10
10
|
<title>Parotta</title>
|
|
11
11
|
)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export
|
|
14
|
+
export const Body = () => {
|
|
15
15
|
const { data, cache } = useFetch("/todos");
|
|
16
16
|
console.log('page');
|
|
17
17
|
console.log('data', data);
|
|
@@ -22,7 +22,7 @@ export default function Page() {
|
|
|
22
22
|
}, [])
|
|
23
23
|
const router = useRouter();
|
|
24
24
|
return (
|
|
25
|
-
<div
|
|
25
|
+
<div>
|
|
26
26
|
<div>
|
|
27
27
|
<h1>Home Page</h1>
|
|
28
28
|
<p>
|
packages/example/routes/todos/:id/page.css
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
.
|
|
1
|
+
.body{
|
|
2
2
|
padding: 10px;
|
|
3
3
|
background-color: turquoise;
|
|
4
|
-
|
|
5
|
-
& .count {
|
|
6
|
-
color: black;
|
|
7
|
-
padding: 40px;
|
|
8
|
-
font-size: 30px;
|
|
9
|
-
font-weight: 600;
|
|
10
|
-
}
|
|
11
4
|
}
|
packages/example/routes/todos/:id/page.jsx
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { Suspense } from "react";
|
|
2
2
|
import { Link, useRouter } from "parotta/router";
|
|
3
3
|
import { useFetch } from "parotta/fetch";
|
|
4
|
-
// import "./index.css";
|
|
5
4
|
|
|
6
|
-
export
|
|
5
|
+
export const Head = () => {
|
|
6
|
+
const { params } = useRouter();
|
|
7
|
+
return (
|
|
8
|
+
<title>Todo: {params.id}</title>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Body = () => {
|
|
7
13
|
const { params } = useRouter();
|
|
8
14
|
const data = useFetch(`/todos/${params.id}`);
|
|
9
15
|
console.log('data', data);
|
|
10
16
|
return (
|
|
11
|
-
<div
|
|
17
|
+
<div>
|
|
12
18
|
<h1>Todo no: {params.id}</h1>
|
|
13
19
|
</div>
|
|
14
20
|
)
|
packages/example/routes/todos/page.css
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
body{
|
|
2
2
|
padding: 10px;
|
|
3
3
|
background-color: turquoise;
|
|
4
|
-
|
|
5
|
-
& .count {
|
|
6
|
-
color: black;
|
|
7
|
-
padding: 40px;
|
|
8
|
-
font-size: 30px;
|
|
9
|
-
font-weight: 600;
|
|
10
|
-
}
|
|
11
4
|
}
|
packages/example/routes/todos/page.jsx
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { Suspense } from "react";
|
|
2
2
|
import TodoList from "@/containers/TodoList/TodoList";
|
|
3
|
-
// import "./index.css";
|
|
4
3
|
|
|
5
|
-
export
|
|
4
|
+
export const Head = () => {
|
|
6
5
|
return (
|
|
7
|
-
<
|
|
6
|
+
<title>Todos</title>
|
|
7
|
+
)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Body = () => {
|
|
11
|
+
return (
|
|
12
|
+
<div>
|
|
8
13
|
<h1>Todos</h1>
|
|
9
14
|
{/* <Suspense>
|
|
10
15
|
<TodoList todos={todos} />
|
packages/parotta/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import postcssNesting from "postcss-nesting";
|
|
|
11
11
|
import { createMemoryHistory } from "history";
|
|
12
12
|
import { createRouter } from 'radix3';
|
|
13
13
|
import mimeTypes from "mime-types";
|
|
14
|
-
import {
|
|
14
|
+
import { Head, Body } from "./router";
|
|
15
15
|
import { renderToReadableStream } from 'react-dom/server';
|
|
16
16
|
// import { renderToStream } from './render';
|
|
17
17
|
|
|
@@ -79,17 +79,17 @@ const serverRouter = createRouter({
|
|
|
79
79
|
|
|
80
80
|
const clientRoutes = clientSideRoutes.reduce((acc, r) => {
|
|
81
81
|
const Head = import(`${process.cwd()}/routes${r === "" ? "" : r}/page.jsx`);
|
|
82
|
-
const
|
|
82
|
+
const Body = import(`${process.cwd()}/routes${r === "" ? "" : r}/page.jsx`);
|
|
83
83
|
acc[r === "" ? "/" : r] = {
|
|
84
84
|
Head,
|
|
85
|
-
|
|
85
|
+
Body,
|
|
86
86
|
}
|
|
87
87
|
return acc
|
|
88
88
|
}, {});
|
|
89
89
|
|
|
90
90
|
for (const k of Object.keys(clientRoutes)) {
|
|
91
91
|
clientRoutes[k].Head = (await clientRoutes[k].Head).Head;
|
|
92
|
-
clientRoutes[k].
|
|
92
|
+
clientRoutes[k].Body = (await clientRoutes[k].Body).Body;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
const clientRouter = createRouter({
|
|
@@ -156,20 +156,18 @@ const renderPage = async (url) => {
|
|
|
156
156
|
const stream = await renderToReadableStream(
|
|
157
157
|
<html lang="en">
|
|
158
158
|
<head>
|
|
159
|
-
<
|
|
159
|
+
<Head
|
|
160
160
|
history={history}
|
|
161
161
|
radixRouter={clientRouter}
|
|
162
162
|
importMap={importMap}
|
|
163
163
|
/>
|
|
164
164
|
</head>
|
|
165
165
|
<body>
|
|
166
|
-
<div id="page">
|
|
167
|
-
|
|
166
|
+
<Body
|
|
168
|
-
|
|
167
|
+
App={React.lazy(() => import(`${process.cwd()}/routes/app.jsx`))}
|
|
169
|
-
|
|
168
|
+
history={history}
|
|
170
|
-
|
|
169
|
+
radixRouter={clientRouter}
|
|
171
|
-
|
|
170
|
+
/>
|
|
172
|
-
</div>
|
|
173
171
|
{config.hydrate &&
|
|
174
172
|
<>
|
|
175
173
|
<script type="module" defer={true} dangerouslySetInnerHTML={{
|
|
@@ -178,7 +176,7 @@ import React from "react";
|
|
|
178
176
|
import { hydrateRoot } from "react-dom/client";
|
|
179
177
|
import { createBrowserHistory } from "history";
|
|
180
178
|
import { createRouter } from "radix3";
|
|
181
|
-
import {
|
|
179
|
+
import { Head, Body } from "parotta/router";
|
|
182
180
|
|
|
183
181
|
const history = createBrowserHistory();
|
|
184
182
|
const radixRouter = createRouter({
|
|
@@ -186,17 +184,17 @@ const radixRouter = createRouter({
|
|
|
186
184
|
routes: {
|
|
187
185
|
${clientSideRoutes.map((r) => `"${r === "" ? "/" : r}": {
|
|
188
186
|
Head: React.lazy(() => import("/routes${r === "" ? "" : r}/page.jsx").then((js) => ({ default: js.Head }))),
|
|
189
|
-
|
|
187
|
+
Body: React.lazy(() => import("/routes${r === "" ? "" : r}/page.jsx").then((js) => ({ default: js.Body }))),
|
|
190
188
|
}`).join(',\n ')}
|
|
191
189
|
},
|
|
192
190
|
});
|
|
193
191
|
|
|
194
|
-
hydrateRoot(document.head, React.createElement(
|
|
192
|
+
hydrateRoot(document.head, React.createElement(Head, {
|
|
195
193
|
history,
|
|
196
194
|
radixRouter,
|
|
197
195
|
}))
|
|
198
196
|
|
|
199
|
-
hydrateRoot(document.
|
|
197
|
+
hydrateRoot(document.body, React.createElement(Body, {
|
|
200
198
|
App: React.lazy(() => import("/routes/app.jsx")),
|
|
201
199
|
history,
|
|
202
200
|
radixRouter,
|
packages/parotta/router.js
CHANGED
|
@@ -13,7 +13,7 @@ const getMatch = (radixRouter, pathname) => {
|
|
|
13
13
|
|
|
14
14
|
const getCssUrl = (pathname) => `/routes${pathname === "/" ? "/page.css" : pathname + "/page.css"}`;
|
|
15
15
|
|
|
16
|
-
export const
|
|
16
|
+
export const Head = ({ history, radixRouter, importMap }) => {
|
|
17
17
|
const pathname = useSyncExternalStore(history.listen, (v) => v ? v.location.pathname : history.location.pathname, () => history.location.pathname);
|
|
18
18
|
const match = getMatch(radixRouter, pathname);
|
|
19
19
|
const initialCss = useMemo(() => getCssUrl(history.location.pathname), []);
|
|
@@ -24,8 +24,9 @@ export const Header = ({ history, radixRouter, importMap }) => {
|
|
|
24
24
|
href: "https://unpkg.com/nprogress@0.2.0/nprogress.css",
|
|
25
25
|
}),
|
|
26
26
|
React.createElement("link", {
|
|
27
|
+
id: "pageCss",
|
|
27
28
|
rel: "stylesheet",
|
|
28
|
-
href:
|
|
29
|
+
href: getCssUrl(history.location.pathname),
|
|
29
30
|
}),
|
|
30
31
|
React.createElement(React.Suspense, {
|
|
31
32
|
children: React.createElement(match.Head, {}),
|
|
@@ -41,32 +42,32 @@ export const Header = ({ history, radixRouter, importMap }) => {
|
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
export const
|
|
45
|
+
export const Body = ({ App, history, radixRouter }) => {
|
|
45
46
|
const [isPending, startTransition] = React.useTransition();
|
|
46
47
|
const [match, setMatch] = useState(() => getMatch(radixRouter, history.location.pathname));
|
|
47
48
|
useEffect(() => {
|
|
48
49
|
return history.listen(({ location }) => {
|
|
49
|
-
const href = getCssUrl(location.pathname);
|
|
50
|
+
// const href = getCssUrl(location.pathname);
|
|
50
|
-
const isLoaded = Array.from(document.getElementsByTagName("link"))
|
|
51
|
+
// const isLoaded = Array.from(document.getElementsByTagName("link"))
|
|
51
|
-
|
|
52
|
+
// .map((link) => link.href.replace(window.origin, "")).includes(href);
|
|
52
|
-
if (!isLoaded) {
|
|
53
|
+
// if (!isLoaded) {
|
|
53
|
-
|
|
54
|
+
// const link = document.createElement('link');
|
|
54
|
-
|
|
55
|
+
// link.setAttribute("rel", "stylesheet");
|
|
55
|
-
|
|
56
|
+
// link.setAttribute("type", "text/css");
|
|
56
|
-
|
|
57
|
+
// link.onload = () => {
|
|
57
|
-
|
|
58
|
+
// nProgress.start();
|
|
58
|
-
|
|
59
|
+
// startTransition(() => {
|
|
59
|
-
|
|
60
|
+
// setMatch(getMatch(radixRouter, location.pathname));
|
|
60
|
-
|
|
61
|
+
// })
|
|
61
|
-
|
|
62
|
+
// };
|
|
62
|
-
|
|
63
|
+
// link.setAttribute("href", href);
|
|
63
|
-
|
|
64
|
+
// document.getElementsByTagName("head")[0].appendChild(link);
|
|
64
|
-
} else {
|
|
65
|
+
// } else {
|
|
65
66
|
nProgress.start();
|
|
66
67
|
startTransition(() => {
|
|
67
68
|
setMatch(getMatch(radixRouter, location.pathname));
|
|
68
69
|
})
|
|
69
|
-
}
|
|
70
|
+
// }
|
|
70
71
|
});
|
|
71
72
|
}, []);
|
|
72
73
|
useEffect(() => {
|
|
@@ -81,7 +82,7 @@ export const Router = ({ App, history, radixRouter }) => {
|
|
|
81
82
|
params: match.params || {},
|
|
82
83
|
},
|
|
83
84
|
children: React.createElement(App, {
|
|
84
|
-
children: React.createElement(match.
|
|
85
|
+
children: React.createElement(match.Body, {}),
|
|
85
86
|
}),
|
|
86
87
|
});
|
|
87
88
|
}
|
readme.md
CHANGED
|
@@ -7,4 +7,6 @@ It is very opionated and has set of idiomatic ways of doing things.
|
|
|
7
7
|
### Todo
|
|
8
8
|
1. Add build step
|
|
9
9
|
2. Deploy to Docker, Deno deploy, Vercel edge functions, Cloudflare workers, Bun edge (whenever it releases)
|
|
10
|
-
3. Hydrate fetch cache
|
|
10
|
+
3. Hydrate fetch cache
|
|
11
|
+
4. Build a proper example (create-parotta-app)
|
|
12
|
+
5. Build a Webiste with Docs using parotta
|