~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


5a97fa61 Peter John

2 years ago
improve stuff
bun.toml DELETED
@@ -1,3 +0,0 @@
1
- [loaders]
2
- ".jsx" = "js"
3
- ".css" = "css"
package-lock.json CHANGED
@@ -1,19 +1,14 @@
1
1
  {
2
- "name": "parotta-workspace",
2
+ "name": "quickstart",
3
3
  "version": "1.0.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
- "name": "parotta-workspace",
9
- "version": "1.0.0",
10
8
  "workspaces": [
11
9
  "packages/example",
12
10
  "packages/parotta"
13
- ],
11
+ ]
14
- "bin": {
15
- "parotta": "package/parotta/cli"
16
- }
17
12
  },
18
13
  "node_modules/js-tokens": {
19
14
  "version": "4.0.0",
@@ -65,16 +60,38 @@
65
60
  "loose-envify": "^1.1.0"
66
61
  }
67
62
  },
63
+ "node_modules/swr": {
64
+ "version": "2.1.0",
65
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.1.0.tgz",
66
+ "integrity": "sha512-4hYl5p3/KalQ1MORealM79g/DtLohmud6yyfXw5l4wjtFksYUnocRFudvyaoUtgj3FrVNK9lS25Av9dsZYvz0g==",
67
+ "dependencies": {
68
+ "use-sync-external-store": "^1.2.0"
69
+ },
70
+ "engines": {
71
+ "pnpm": "7"
72
+ },
73
+ "peerDependencies": {
74
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0"
75
+ }
76
+ },
77
+ "node_modules/use-sync-external-store": {
78
+ "version": "1.2.0",
79
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
80
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
81
+ "peerDependencies": {
82
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
83
+ }
84
+ },
68
85
  "packages/example": {
69
86
  "name": "quickstart",
70
87
  "dependencies": {
71
- "parotta": "0.1.0",
72
88
  "react": "18.2.0",
73
- "react-dom": "^18.2.0"
89
+ "react-dom": "^18.2.0",
90
+ "swr": "2.1.0"
74
91
  }
75
92
  },
76
93
  "packages/parotta": {
77
- "version": "0.1.0",
94
+ "version": "0.2.0",
78
95
  "dependencies": {
79
96
  "autoprefixer": "^10.4.14",
80
97
  "mime-types": "2.1.35",
@@ -86,10 +103,7 @@
86
103
  "walkdir": "0.4.1"
87
104
  },
88
105
  "bin": {
89
- "parotta": "cli"
106
+ "parotta": "cli.js"
90
- },
91
- "devDependencies": {
92
- "bun-types": "^0.5.0"
93
107
  }
94
108
  },
95
109
  "packages/parotta/node_modules/@csstools/cascade-layer-name-parser": {
@@ -223,10 +237,6 @@
223
237
  "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
224
238
  }
225
239
  },
226
- "packages/parotta/node_modules/bun-types": {
227
- "version": "0.5.7",
228
- "dev": true
229
- },
230
240
  "packages/parotta/node_modules/caniuse-lite": {
231
241
  "version": "1.0.30001468",
232
242
  "funding": [
@@ -482,7 +492,6 @@
482
492
  "version": "file:packages/parotta",
483
493
  "requires": {
484
494
  "autoprefixer": "^10.4.14",
485
- "bun-types": "^0.5.0",
486
495
  "mime-types": "2.1.35",
487
496
  "postcss": "^8.4.21",
488
497
  "postcss-custom-media": "^9.1.2",
@@ -534,10 +543,6 @@
534
543
  "update-browserslist-db": "^1.0.10"
535
544
  }
536
545
  },
537
- "bun-types": {
538
- "version": "0.5.7",
539
- "dev": true
540
- },
541
546
  "caniuse-lite": {
542
547
  "version": "1.0.30001468"
543
548
  },
@@ -647,9 +652,9 @@
647
652
  "quickstart": {
648
653
  "version": "file:packages/example",
649
654
  "requires": {
650
- "parotta": "0.1.0",
651
655
  "react": "18.2.0",
652
- "react-dom": "^18.2.0"
656
+ "react-dom": "^18.2.0",
657
+ "swr": "2.1.0"
653
658
  }
654
659
  },
655
660
  "react": {
@@ -670,6 +675,20 @@
670
675
  "requires": {
671
676
  "loose-envify": "^1.1.0"
672
677
  }
678
+ },
679
+ "swr": {
680
+ "version": "2.1.0",
681
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.1.0.tgz",
682
+ "integrity": "sha512-4hYl5p3/KalQ1MORealM79g/DtLohmud6yyfXw5l4wjtFksYUnocRFudvyaoUtgj3FrVNK9lS25Av9dsZYvz0g==",
683
+ "requires": {
684
+ "use-sync-external-store": "^1.2.0"
685
+ }
686
+ },
687
+ "use-sync-external-store": {
688
+ "version": "1.2.0",
689
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
690
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
691
+ "requires": {}
673
692
  }
674
693
  }
675
694
  }
packages/example/bun.lockb ADDED
Binary file
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
- // const router = useRouter();
10
+ const router = useRouter();
7
11
  return (
8
12
  <div className="home-page">
9
13
  <div>
10
14
  <h1>Home Page</h1>
11
- {/* <p>
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 { routerSignal } from './router.js';
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 = (filePath, initialRouteValue) => {
65
+ const hydrateScript = (appPath, pagePath, routerProps) => {
69
66
  return `
70
- import React from 'react';
67
+ import React from "react";
71
- import { hydrateRoot } from 'react-dom/client';
68
+ import { hydrateRoot } from "react-dom/client";
72
- import { routerSignal } from "parotta/router";
69
+ import App from "${removeCwd(appPath)}";
73
- import Page from "${filePath}";
70
+ import Page from "${removeCwd(pagePath)}";
74
71
 
75
- routerSignal.value = ${JSON.stringify(initialRouteValue)};
72
+ const routerProps = ${JSON.stringify(routerProps)};
76
73
 
77
- hydrateRoot(document.getElementById("root"), React.createElement(Page, {}, undefined, false, undefined, this));
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 initialRouteValue = {
116
+ const routerProps = {
117
117
  query: query,
118
118
  params: params,
119
119
  pathname: url.pathname,
120
120
  }
121
- routerSignal.value = initialRouteValue;
121
+ const appPath = path.join(process.cwd(), "routes", "app.jsx");
122
- const routeImport = await import(path.join(process.cwd(), filePath));
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]}${devTag}`;
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 = routeImport.default;
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-dom/client": `https://esm.sh/react-dom@18.2.0/client${devTag}`,
144
+ "react": `https://esm.sh/react@18.2.0`,
145
- "react/jsx-dev-runtime": `https://esm.sh/react@18.2.0/jsx-dev-runtime${devTag}`,
145
+ "react/jsx-dev-runtime": `https://esm.sh/react@18.2.0/jsx-dev-runtime`,
146
+ "react-dom/client": `https://esm.sh/react-dom@18.2.0/client`,
147
+ "react-streaming": "https://esm.sh/react-streaming",
146
- "parotta/router": `https://esm.sh/parotta@${parottaVersion}`,
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(filePath, initialRouteValue)
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
- <Page />
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 (url) => {
212
+ const renderJs = async (src) => {
197
213
  try {
198
- const jsText = await Bun.file(path.join(process.cwd(), url.pathname)).text();
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) {