edge-city
git clone https://git.pyrossh.dev/edge-city
edge-city is a next level meta-framework for react that runs only on edge runtimes
5a97fa6
— Peter John
2023-03-21T22:48:30+05:30
improve stuff
Generated files not shown: package-lock.json, packages/example/bun.lockb
- bun.toml +0 -3
- packages/example/components/Counter/Counter.jsx +1 -1
- packages/example/components/Todo/Todo.jsx +2 -0
- packages/example/containers/TodoList/TodoList.jsx +1 -0
- packages/example/jsconfig.json +5 -1
- packages/example/package.json +2 -2
- packages/example/routes/about/page.css +3 -0
- packages/example/routes/about/page.jsx +19 -0
- packages/example/routes/app.jsx +26 -0
- packages/example/routes/page.css +4 -0
- packages/example/routes/page.jsx +11 -4
- packages/parotta/cli.js +53 -31
bun.toml
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
[loaders]
|
|
2
|
-
".jsx" = "js"
|
|
3
|
-
".css" = "css"
|
packages/example/components/Counter/Counter.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import React, { useState } from "react";
|
|
2
2
|
|
|
3
3
|
const Counter = () => {
|
|
4
4
|
const [count, setCount] = useState(5);
|
packages/example/components/Todo/Todo.jsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
1
3
|
const Todo = ({ id, text }) => {
|
|
2
4
|
return (
|
|
3
5
|
<div id={id}>
|
packages/example/containers/TodoList/TodoList.jsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import Todo from "@/components/Todo/Todo";
|
|
2
3
|
|
|
3
4
|
const todos = [
|
packages/example/jsconfig.json
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
"@/*": [
|
|
5
5
|
"./*"
|
|
6
6
|
]
|
|
7
|
-
}
|
|
7
|
+
},
|
|
8
|
+
// TODO these options are not supported
|
|
9
|
+
"jsx": "react",
|
|
10
|
+
"jsxFactory": "React.createElement",
|
|
11
|
+
"jsxImportSource": "react"
|
|
8
12
|
}
|
|
9
13
|
}
|
packages/example/package.json
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"run": "docker run -p 3000:3000 example"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"parotta": "0.1.0",
|
|
12
11
|
"react": "18.2.0",
|
|
13
|
-
"react-dom": "^18.2.0"
|
|
12
|
+
"react-dom": "^18.2.0",
|
|
13
|
+
"swr": "2.1.0"
|
|
14
14
|
}
|
|
15
15
|
}
|
packages/example/routes/about/page.css
CHANGED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
.about-page {
|
|
2
|
+
padding: 100px;
|
|
3
|
+
}
|
packages/example/routes/about/page.jsx
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useRouter } from "parotta/router";
|
|
3
|
+
import "./page.css";
|
|
4
|
+
|
|
5
|
+
const AboutPage = () => {
|
|
6
|
+
const router = useRouter();
|
|
7
|
+
return (
|
|
8
|
+
<div className="about-page">
|
|
9
|
+
<div>
|
|
10
|
+
<h1>About Page</h1>
|
|
11
|
+
<p>
|
|
12
|
+
Path: {router.pathname}
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default AboutPage;
|
packages/example/routes/app.jsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React, { Suspense } from "react";
|
|
2
|
+
import { SWRConfig } from "swr";
|
|
3
|
+
import { RouterProvider } from "parotta/router";
|
|
4
|
+
import { ErrorBoundary } from "parotta/error";
|
|
5
|
+
|
|
6
|
+
const App = ({ routerProps, children }) => {
|
|
7
|
+
return (
|
|
8
|
+
<RouterProvider value={routerProps}>
|
|
9
|
+
<SWRConfig value={{
|
|
10
|
+
fallback: {
|
|
11
|
+
'https://jsonplaceholder.typicode.com/todos/1': { id: '123' },
|
|
12
|
+
},
|
|
13
|
+
// fallbackData: null,
|
|
14
|
+
fetcher: (resource, init) => fetch(resource, init).then(res => res.json()), suspense: true
|
|
15
|
+
}}>
|
|
16
|
+
<ErrorBoundary onError={(err) => console.log(err)} fallback={<p>Oops something went wrong</p>}>
|
|
17
|
+
<Suspense fallback={<p>Loading...</p>}>
|
|
18
|
+
{children}
|
|
19
|
+
</Suspense>
|
|
20
|
+
</ErrorBoundary>
|
|
21
|
+
</SWRConfig>
|
|
22
|
+
</RouterProvider>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default App;
|
packages/example/routes/page.css
CHANGED
|
@@ -8,4 +8,8 @@
|
|
|
8
8
|
font-size: 30px;
|
|
9
9
|
font-weight: 600;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
& footer {
|
|
13
|
+
margin-top: 100px;
|
|
14
|
+
}
|
|
11
15
|
}
|
packages/example/routes/page.jsx
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import { useRouter } from "parotta/router";
|
|
1
4
|
import Counter from "@/components/Counter/Counter";
|
|
2
|
-
// import { useRouter } from "muffinjs/router.js";
|
|
3
5
|
import "./page.css";
|
|
4
6
|
|
|
5
7
|
const HomePage = () => {
|
|
8
|
+
// const todo = useAsync('123', () => getData());
|
|
9
|
+
const { data } = useSWR(`https://jsonplaceholder.typicode.com/todos/1`);
|
|
6
|
-
|
|
10
|
+
const router = useRouter();
|
|
7
11
|
return (
|
|
8
12
|
<div className="home-page">
|
|
9
13
|
<div>
|
|
10
14
|
<h1>Home Page</h1>
|
|
11
|
-
|
|
15
|
+
<p>
|
|
12
16
|
Path: {router.pathname}
|
|
13
|
-
</p>
|
|
17
|
+
</p>
|
|
14
18
|
<Counter />
|
|
15
19
|
</div>
|
|
20
|
+
<footer>
|
|
21
|
+
<a href="/about">About us</a>
|
|
22
|
+
</footer>
|
|
16
23
|
</div>
|
|
17
24
|
)
|
|
18
25
|
}
|
packages/parotta/cli.js
CHANGED
|
@@ -7,24 +7,19 @@ import autoprefixer from "autoprefixer";
|
|
|
7
7
|
import postcssCustomMedia from "postcss-custom-media";
|
|
8
8
|
// import postcssNormalize from 'postcss-normalize';
|
|
9
9
|
import postcssNesting from "postcss-nesting";
|
|
10
|
-
import { renderToReadableStream } from 'react-dom/server';
|
|
11
10
|
import { createRouter } from 'radix3';
|
|
12
11
|
import mimeTypes from "mime-types";
|
|
12
|
+
import { renderToReadableStream } from 'react-dom/server';
|
|
13
|
-
import {
|
|
13
|
+
// import { renderToStream } from './render';
|
|
14
14
|
|
|
15
|
+
const version = (await import(path.join(import.meta.dir, "package.json"))).default.version;
|
|
16
|
+
console.log(`parotta v${version}`)
|
|
15
17
|
console.log("running in folder:", path.basename(process.cwd()), "env:", process.env.NODE_ENV);
|
|
16
18
|
// console.log("deleting cache");
|
|
17
19
|
// rmSync(path.join(process.cwd(), ".cache"), { force: true, recursive: true })
|
|
18
20
|
|
|
19
21
|
const isProd = process.env.NODE_ENV === "production";
|
|
20
22
|
|
|
21
|
-
const transpiler = new Bun.Transpiler({
|
|
22
|
-
loader: "jsx",
|
|
23
|
-
autoImportJSX: true,
|
|
24
|
-
jsxOptimizationInline: true,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
|
|
28
23
|
const mapFiles = () => {
|
|
29
24
|
const routes = {};
|
|
30
25
|
const dirs = walkdir.sync(path.join(process.cwd(), "routes"))
|
|
@@ -65,16 +60,21 @@ const mapDeps = (dir) => {
|
|
|
65
60
|
}, {});
|
|
66
61
|
}
|
|
67
62
|
|
|
63
|
+
const removeCwd = (s) => s.replace(process.cwd(), "")
|
|
64
|
+
|
|
68
|
-
const hydrateScript = (
|
|
65
|
+
const hydrateScript = (appPath, pagePath, routerProps) => {
|
|
69
66
|
return `
|
|
70
|
-
import React from
|
|
67
|
+
import React from "react";
|
|
71
|
-
import { hydrateRoot } from
|
|
68
|
+
import { hydrateRoot } from "react-dom/client";
|
|
72
|
-
import
|
|
69
|
+
import App from "${removeCwd(appPath)}";
|
|
73
|
-
import Page from "${
|
|
70
|
+
import Page from "${removeCwd(pagePath)}";
|
|
74
71
|
|
|
75
|
-
|
|
72
|
+
const routerProps = ${JSON.stringify(routerProps)};
|
|
76
73
|
|
|
77
|
-
hydrateRoot(document.getElementById("root"), React.createElement(
|
|
74
|
+
hydrateRoot(document.getElementById("root"), React.createElement(App, {
|
|
75
|
+
routerProps: routerProps,
|
|
76
|
+
children: React.createElement(Page, {}),
|
|
77
|
+
}));
|
|
78
78
|
`;
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -113,23 +113,23 @@ const renderPage = async (filePath, url, params) => {
|
|
|
113
113
|
for (const key of url.searchParams.keys()) {
|
|
114
114
|
query[key] = url.searchParams.get(key);
|
|
115
115
|
}
|
|
116
|
-
const
|
|
116
|
+
const routerProps = {
|
|
117
117
|
query: query,
|
|
118
118
|
params: params,
|
|
119
119
|
pathname: url.pathname,
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
const appPath = path.join(process.cwd(), "routes", "app.jsx");
|
|
122
|
-
const
|
|
122
|
+
const pagePath = path.join(process.cwd(), filePath);
|
|
123
123
|
const packageJson = await import(path.join(process.cwd(), "package.json"));
|
|
124
124
|
const devTag = !isProd ? "?dev" : "";
|
|
125
125
|
const nodeDeps = Object.keys(packageJson.default.dependencies).reduce((acc, dep) => {
|
|
126
|
-
acc[dep] = `https://esm.sh/${dep}@${packageJson.default.dependencies[dep]}
|
|
126
|
+
acc[dep] = `https://esm.sh/${dep}@${packageJson.default.dependencies[dep]}`;
|
|
127
127
|
return acc;
|
|
128
128
|
}, {})
|
|
129
|
+
const App = (await import(appPath)).default;
|
|
129
|
-
const Page =
|
|
130
|
+
const Page = (await import(pagePath)).default;
|
|
130
131
|
const components = mapDeps("components");
|
|
131
132
|
const containers = mapDeps("containers");
|
|
132
|
-
const parottaVersion = packageJson.default.dependencies["parotta"];
|
|
133
133
|
const cssFile = `${filePath.replace("jsx", "css")}`;
|
|
134
134
|
const stream = await renderToReadableStream(
|
|
135
135
|
<html lang="en">
|
|
@@ -141,9 +141,12 @@ const renderPage = async (filePath, url, params) => {
|
|
|
141
141
|
{
|
|
142
142
|
"imports": {
|
|
143
143
|
"radix3": `https://esm.sh/radix3`,
|
|
144
|
-
"react
|
|
144
|
+
"react": `https://esm.sh/[email protected]`,
|
|
145
|
-
"react/jsx-dev-runtime": `https://esm.sh/[email protected]/jsx-dev-runtime
|
|
145
|
+
"react/jsx-dev-runtime": `https://esm.sh/[email protected]/jsx-dev-runtime`,
|
|
146
|
+
"react-dom/client": `https://esm.sh/[email protected]/client`,
|
|
147
|
+
"react-streaming": "https://esm.sh/react-streaming",
|
|
146
|
-
"parotta/router": `https://esm.sh/parotta@${
|
|
148
|
+
"parotta/router": `https://esm.sh/parotta@${version}/router.js`,
|
|
149
|
+
"parotta/error": `https://esm.sh/parotta@${version}/error.js`,
|
|
147
150
|
...nodeDeps,
|
|
148
151
|
...components,
|
|
149
152
|
...containers,
|
|
@@ -152,8 +155,8 @@ const renderPage = async (filePath, url, params) => {
|
|
|
152
155
|
)
|
|
153
156
|
}}>
|
|
154
157
|
</script>
|
|
155
|
-
<script type="module" defer dangerouslySetInnerHTML={{
|
|
158
|
+
<script type="module" defer={true} dangerouslySetInnerHTML={{
|
|
156
|
-
__html: hydrateScript(
|
|
159
|
+
__html: hydrateScript(appPath, pagePath, routerProps)
|
|
157
160
|
}}></script>
|
|
158
161
|
<title>
|
|
159
162
|
Parotta
|
|
@@ -161,11 +164,14 @@ const renderPage = async (filePath, url, params) => {
|
|
|
161
164
|
</head>
|
|
162
165
|
<body>
|
|
163
166
|
<div id="root">
|
|
167
|
+
<App routerProps={routerProps}>
|
|
164
|
-
|
|
168
|
+
<Page />
|
|
169
|
+
</App>
|
|
165
170
|
</div>
|
|
166
171
|
</body>
|
|
167
172
|
</html >
|
|
168
173
|
);
|
|
174
|
+
// injectToStream('<script type="module" src="/main.js"></script>', { flush: true });
|
|
169
175
|
return new Response(stream, {
|
|
170
176
|
headers: { 'Content-Type': 'text/html' },
|
|
171
177
|
status: 200,
|
|
@@ -193,11 +199,23 @@ const renderCss = async (url) => {
|
|
|
193
199
|
}
|
|
194
200
|
}
|
|
195
201
|
|
|
202
|
+
const transpiler = new Bun.Transpiler({
|
|
203
|
+
loader: "jsx",
|
|
204
|
+
autoImportJSX: true,
|
|
205
|
+
jsxOptimizationInline: true,
|
|
206
|
+
|
|
207
|
+
// TODO
|
|
208
|
+
// autoImportJSX: false,
|
|
209
|
+
// jsxOptimizationInline: false,
|
|
210
|
+
});
|
|
211
|
+
|
|
196
|
-
const renderJs = async (
|
|
212
|
+
const renderJs = async (src) => {
|
|
197
213
|
try {
|
|
198
|
-
const jsText = await Bun.file(
|
|
214
|
+
const jsText = await Bun.file(src).text();
|
|
199
215
|
const result = await transpiler.transform(jsText);
|
|
200
216
|
const js = result.replaceAll(`import"./page.css";`, "");
|
|
217
|
+
// TODO
|
|
218
|
+
//.replaceAll("$jsx", "React.createElement");
|
|
201
219
|
return new Response(js, {
|
|
202
220
|
headers: { 'Content-Type': 'application/javascript' },
|
|
203
221
|
status: 200,
|
|
@@ -234,8 +252,12 @@ export default {
|
|
|
234
252
|
return renderCss(url);
|
|
235
253
|
}
|
|
236
254
|
if (url.pathname.endsWith(".js") || url.pathname.endsWith(".jsx")) {
|
|
237
|
-
return renderJs(url);
|
|
255
|
+
return renderJs(path.join(process.cwd(), url.pathname));
|
|
238
256
|
}
|
|
257
|
+
// maybe this is needed
|
|
258
|
+
// if (url.pathname.startsWith("/parotta/")) {
|
|
259
|
+
// return renderJs(path.join(import.meta.dir, url.pathname.replace("/parotta/", "")) + ".js");
|
|
260
|
+
// }
|
|
239
261
|
const match = radixRouter.lookup(url.pathname);
|
|
240
262
|
if (match) {
|
|
241
263
|
if (match.file) {
|