~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
5636e804
—
Peter John 2 years ago
fix merge
- .gitignore +1 -1
- .vscode/extensions.json +0 -5
- .vscode/settings.json +0 -13
- Dockerfile +0 -8
- bun.lockb +0 -0
- components/Counter/Counter.jsx +0 -16
- components/Layout/Layout.css +0 -5
- components/Layout/Layout.jsx +0 -19
- components/Timer/Timer.jsx +0 -18
- components/Todo/Todo.css +0 -16
- components/Todo/Todo.jsx +0 -94
- db/index.js +0 -29
- db/migrations/0000_empty_shatterstar.sql +0 -7
- db/migrations/meta/0000_snapshot.json +0 -54
- db/migrations/meta/_journal.json +0 -12
- drizzle.config.json +0 -4
- index.js +7 -5
- jsconfig.json +0 -12
- main.js +0 -6
- package.json +5 -1
- pages/_404/page.css +0 -38
- pages/_404/page.jsx +0 -19
- pages/_500/page.css +0 -38
- pages/_500/page.jsx +0 -19
- pages/about/page.css +0 -10
- pages/about/page.jsx +0 -28
- pages/page.css +0 -17
- pages/page.jsx +0 -29
- pages/page.spec.js +0 -19
- pages/todos/page.css +0 -71
- pages/todos/page.jsx +0 -61
- parotta/bun.lockb +0 -0
- parotta/package.json +0 -18
- parotta/readme.md +0 -13
- parotta/runtime.js +0 -224
- parotta/server.js +0 -320
- playwright.config.js +0 -65
- pnpm-lock.yaml +8 -0
- readme.md +19 -5
- services/auth.service.js +0 -50
- services/todos.service.js +0 -28
- static/favicon.ico +0 -0
- static/logo192.png +0 -0
- static/logo512.png +0 -0
- static/manifest.json +0 -25
- static/robots.txt +0 -3
.gitignore
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
node_modules
|
|
1
|
+
node_modules
|
.vscode/extensions.json
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"recommendations": [
|
|
3
|
-
"ms-playwright.playwright"
|
|
4
|
-
]
|
|
5
|
-
}
|
.vscode/settings.json
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"files.exclude": {
|
|
3
|
-
"**/.git": true,
|
|
4
|
-
"**/.svn": true,
|
|
5
|
-
"**/.hg": true,
|
|
6
|
-
"**/CVS": true,
|
|
7
|
-
"**/.DS_Store": true,
|
|
8
|
-
"**/Thumbs.db": true,
|
|
9
|
-
"**/dist": true,
|
|
10
|
-
"**/playwright-report": true,
|
|
11
|
-
"test-results": true
|
|
12
|
-
}
|
|
13
|
-
}
|
Dockerfile
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
FROM oven/bun:0.5.8
|
|
2
|
-
|
|
3
|
-
ENV NODE_ENV production
|
|
4
|
-
|
|
5
|
-
WORKDIR /app
|
|
6
|
-
COPY . /app
|
|
7
|
-
|
|
8
|
-
CMD ["bun", "start"]
|
bun.lockb
DELETED
|
Binary file
|
components/Counter/Counter.jsx
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
|
|
3
|
-
const Counter = () => {
|
|
4
|
-
const [count, setCount] = useState(5);
|
|
5
|
-
return (
|
|
6
|
-
<div>
|
|
7
|
-
<button onClick={() => setCount(count - 1)}>-</button>
|
|
8
|
-
<span className="count">
|
|
9
|
-
{count}
|
|
10
|
-
</span>
|
|
11
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
12
|
-
</div>
|
|
13
|
-
)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default Counter;
|
components/Layout/Layout.css
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
.layout-header {
|
|
2
|
-
& a {
|
|
3
|
-
margin-right: 20px;
|
|
4
|
-
}
|
|
5
|
-
}
|
components/Layout/Layout.jsx
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Link } from "parotta/runtime";
|
|
3
|
-
import "./Layout.css";
|
|
4
|
-
|
|
5
|
-
const Layout = ({ children }) => {
|
|
6
|
-
return (
|
|
7
|
-
<div>
|
|
8
|
-
<header className="layout-header">
|
|
9
|
-
<Link href="/about">About us</Link>
|
|
10
|
-
<Link href="/todos">Todos</Link>
|
|
11
|
-
</header>
|
|
12
|
-
<div>
|
|
13
|
-
{children}
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default Layout;
|
components/Timer/Timer.jsx
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
export default function Timer() {
|
|
4
|
-
const [counter, setCounter] = useState(0);
|
|
5
|
-
useEffect(() => {
|
|
6
|
-
const ref = setInterval(() => {
|
|
7
|
-
setCounter((c) => c + 1);
|
|
8
|
-
}, 100);
|
|
9
|
-
return () => {
|
|
10
|
-
clearInterval(ref);
|
|
11
|
-
}
|
|
12
|
-
}, []);
|
|
13
|
-
return (
|
|
14
|
-
<div>
|
|
15
|
-
<p>(This page is interactive while data is loading: {counter})</p>
|
|
16
|
-
</div>
|
|
17
|
-
);
|
|
18
|
-
}
|
components/Todo/Todo.css
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
.todo {
|
|
2
|
-
& label {
|
|
3
|
-
list-style-type: none;
|
|
4
|
-
padding: 1em;
|
|
5
|
-
border-radius: 0.5em;
|
|
6
|
-
background-color: #ddd;
|
|
7
|
-
margin-top: 1em;
|
|
8
|
-
display: flex;
|
|
9
|
-
justify-content: space-between;
|
|
10
|
-
align-items: center;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
& .done {
|
|
14
|
-
text-decoration: line-through;
|
|
15
|
-
}
|
|
16
|
-
}
|
components/Todo/Todo.jsx
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
// import PropTypes from 'prop-types';
|
|
3
|
-
// import { Button, InputGroup } from "@blueprintjs/core";
|
|
4
|
-
// import useMutation from '@/hooks/useMutation';
|
|
5
|
-
// import { TodoPropType } from '@/models/Todo';
|
|
6
|
-
import "./Todo.css";
|
|
7
|
-
|
|
8
|
-
// const propTypes = {
|
|
9
|
-
// // todo: PropTypes.shape(TodoPropType).isRequired,
|
|
10
|
-
// }
|
|
11
|
-
|
|
12
|
-
const Todo = ({ todo }) => {
|
|
13
|
-
const [state, setState] = useState({ text: todo.text, editing: false });
|
|
14
|
-
// const updateMutation = useMutation(async (data) => {
|
|
15
|
-
// await onUpdate({ ...todo, ...data });
|
|
16
|
-
// await refetch();
|
|
17
|
-
// });
|
|
18
|
-
// const deleteMutation = useMutation(async () => {
|
|
19
|
-
// await onDelete(todo.id);
|
|
20
|
-
// await refetch();
|
|
21
|
-
// })
|
|
22
|
-
return (
|
|
23
|
-
<li className="todo">
|
|
24
|
-
{!state.editing && (
|
|
25
|
-
<label>
|
|
26
|
-
<input
|
|
27
|
-
type="checkbox"
|
|
28
|
-
checked={todo.completed}
|
|
29
|
-
onChange={(e) => {
|
|
30
|
-
// updateMutation.mutate({ completed: e.target.checked })
|
|
31
|
-
}}
|
|
32
|
-
/>{" "}
|
|
33
|
-
<span className={todo.completed ? "done" : undefined}>{todo.text}</span>{" "}
|
|
34
|
-
</label>
|
|
35
|
-
)}
|
|
36
|
-
|
|
37
|
-
{/* {state.editing && (
|
|
38
|
-
<InputGroup
|
|
39
|
-
autoFocus
|
|
40
|
-
value={state.text}
|
|
41
|
-
onChange={(e) => setState({ text: e.target.value, editing: true })}
|
|
42
|
-
onKeyDown={async (e) => {
|
|
43
|
-
if (e.key === "Enter") {
|
|
44
|
-
await updateMutation.mutate({ text: state.text });
|
|
45
|
-
setState({ text: todo.text, editing: false });
|
|
46
|
-
} else if (e.key === "Escape") {
|
|
47
|
-
setState({ text: todo.text, editing: false });
|
|
48
|
-
}
|
|
49
|
-
}}
|
|
50
|
-
/>
|
|
51
|
-
)}
|
|
52
|
-
|
|
53
|
-
<span>
|
|
54
|
-
{!todo.completed && !state.editing && (
|
|
55
|
-
<Button
|
|
56
|
-
onClick={() => setState({ text: todo.text, editing: true })}
|
|
57
|
-
>
|
|
58
|
-
Edit
|
|
59
|
-
</Button>
|
|
60
|
-
)}
|
|
61
|
-
|
|
62
|
-
{todo.completed && (
|
|
63
|
-
<Button loading={deleteMutation.isMutating} onClick={deleteMutation.mutate}>
|
|
64
|
-
Delete
|
|
65
|
-
</Button>
|
|
66
|
-
)}
|
|
67
|
-
|
|
68
|
-
{state.editing && state.text !== todo.text && (
|
|
69
|
-
<Button
|
|
70
|
-
loading={updateMutation.isMutating}
|
|
71
|
-
onClick={async () => {
|
|
72
|
-
await updateMutation.mutate({ text: state.text });
|
|
73
|
-
setState({ text: todo.text, editing: false });
|
|
74
|
-
}}
|
|
75
|
-
>
|
|
76
|
-
Save
|
|
77
|
-
</Button>
|
|
78
|
-
)}
|
|
79
|
-
|
|
80
|
-
{state.editing && (
|
|
81
|
-
<Button
|
|
82
|
-
onClick={() => setState({ text: todo.text, editing: false })}
|
|
83
|
-
>
|
|
84
|
-
Cancel
|
|
85
|
-
</Button>
|
|
86
|
-
)}
|
|
87
|
-
</span> */}
|
|
88
|
-
</li>
|
|
89
|
-
);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Todo.propTypes = propTypes;
|
|
93
|
-
|
|
94
|
-
export default Todo;
|
db/index.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { boolean, date, pgTable, serial, text } from 'drizzle-orm/pg-core';
|
|
2
|
-
import { drizzle } from 'drizzle-orm/neon-serverless';
|
|
3
|
-
import { Pool } from '@neondatabase/serverless';
|
|
4
|
-
import { migrate } from 'drizzle-orm/neon-serverless/migrator';
|
|
5
|
-
import { highlight } from 'sql-highlight';
|
|
6
|
-
|
|
7
|
-
export const pool = new Pool({ connectionString: process.env.PG_CONN_URL });
|
|
8
|
-
const db = drizzle(pool, {
|
|
9
|
-
logger: {
|
|
10
|
-
logQuery: (query, params) => {
|
|
11
|
-
const sqlString = params.reduce((acc, v, i) => acc.replaceAll("$" + (i + 1), v), query);
|
|
12
|
-
console.log(highlight(sqlString));
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
export default db;
|
|
18
|
-
|
|
19
|
-
export const migrateAll = async () => {
|
|
20
|
-
await migrate(db, { migrationsFolder: './db/migrations' });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const todos = pgTable('todos', {
|
|
24
|
-
id: serial('id').primaryKey(),
|
|
25
|
-
text: text('text').notNull(),
|
|
26
|
-
completed: boolean('completed').notNull(),
|
|
27
|
-
createdAt: date('createdAt').notNull(),
|
|
28
|
-
updatedAt: date('updatedAt'),
|
|
29
|
-
});
|
db/migrations/0000_empty_shatterstar.sql
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
CREATE TABLE IF NOT EXISTS "todos" (
|
|
2
|
-
"id" serial PRIMARY KEY NOT NULL,
|
|
3
|
-
"text" text NOT NULL,
|
|
4
|
-
"completed" boolean NOT NULL,
|
|
5
|
-
"createdAt" date NOT NULL,
|
|
6
|
-
"updatedAt" date
|
|
7
|
-
);
|
db/migrations/meta/0000_snapshot.json
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "5",
|
|
3
|
-
"dialect": "pg",
|
|
4
|
-
"id": "7b876e3f-3db0-4282-b1c9-a5961b930b99",
|
|
5
|
-
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
6
|
-
"tables": {
|
|
7
|
-
"todos": {
|
|
8
|
-
"name": "todos",
|
|
9
|
-
"schema": "",
|
|
10
|
-
"columns": {
|
|
11
|
-
"id": {
|
|
12
|
-
"name": "id",
|
|
13
|
-
"type": "serial",
|
|
14
|
-
"primaryKey": true,
|
|
15
|
-
"notNull": true
|
|
16
|
-
},
|
|
17
|
-
"text": {
|
|
18
|
-
"name": "text",
|
|
19
|
-
"type": "text",
|
|
20
|
-
"primaryKey": false,
|
|
21
|
-
"notNull": true
|
|
22
|
-
},
|
|
23
|
-
"completed": {
|
|
24
|
-
"name": "completed",
|
|
25
|
-
"type": "boolean",
|
|
26
|
-
"primaryKey": false,
|
|
27
|
-
"notNull": true
|
|
28
|
-
},
|
|
29
|
-
"createdAt": {
|
|
30
|
-
"name": "createdAt",
|
|
31
|
-
"type": "date",
|
|
32
|
-
"primaryKey": false,
|
|
33
|
-
"notNull": true
|
|
34
|
-
},
|
|
35
|
-
"updatedAt": {
|
|
36
|
-
"name": "updatedAt",
|
|
37
|
-
"type": "date",
|
|
38
|
-
"primaryKey": false,
|
|
39
|
-
"notNull": false
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"indexes": {},
|
|
43
|
-
"foreignKeys": {},
|
|
44
|
-
"compositePrimaryKeys": {}
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
"enums": {},
|
|
48
|
-
"schemas": {},
|
|
49
|
-
"_meta": {
|
|
50
|
-
"schemas": {},
|
|
51
|
-
"tables": {},
|
|
52
|
-
"columns": {}
|
|
53
|
-
}
|
|
54
|
-
}
|
db/migrations/meta/_journal.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "5",
|
|
3
|
-
"dialect": "pg",
|
|
4
|
-
"entries": [
|
|
5
|
-
{
|
|
6
|
-
"idx": 0,
|
|
7
|
-
"version": "5",
|
|
8
|
-
"when": 1683227001598,
|
|
9
|
-
"tag": "0000_empty_shatterstar"
|
|
10
|
-
}
|
|
11
|
-
]
|
|
12
|
-
}
|
drizzle.config.json
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"out": "./db/migrations/",
|
|
3
|
-
"schema": "./db/index.js"
|
|
4
|
-
}
|
index.js
CHANGED
|
@@ -245,6 +245,7 @@ export const NavLink = ({ children, className, activeClassName, ...props }) => {
|
|
|
245
245
|
*/
|
|
246
246
|
export const renderPage = async (PageComponent, req) => {
|
|
247
247
|
const { renderToReadableStream } = await import("react-dom/server");
|
|
248
|
+
const { default: isbot } = await import("isbot");
|
|
248
249
|
const url = new URL(req.url);
|
|
249
250
|
const history = createMemoryHistory({
|
|
250
251
|
initialEntries: [url.pathname + url.search],
|
|
@@ -281,11 +282,12 @@ export const renderPage = async (PageComponent, req) => {
|
|
|
281
282
|
})
|
|
282
283
|
})]
|
|
283
284
|
}));
|
|
285
|
+
|
|
286
|
+
if (isbot(req.headers.get('User-Agent'))) {
|
|
287
|
+
await stream.allReady
|
|
284
|
-
|
|
288
|
+
// TODO:
|
|
285
|
-
// if (bot || isCrawler) {
|
|
286
|
-
// await stream.allReady
|
|
287
|
-
|
|
289
|
+
// add helmetContext to head
|
|
288
|
-
|
|
290
|
+
}
|
|
289
291
|
return new Response(stream, {
|
|
290
292
|
headers: { 'Content-Type': 'text/html' },
|
|
291
293
|
status: 200,
|
jsconfig.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"paths": {
|
|
4
|
-
"@/*": [
|
|
5
|
-
"./*"
|
|
6
|
-
]
|
|
7
|
-
},
|
|
8
|
-
// TODO these options are not supported
|
|
9
|
-
"jsx": "react",
|
|
10
|
-
"jsxFactory": "React.createElement"
|
|
11
|
-
}
|
|
12
|
-
}
|
main.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import server from "parotta/server.js";
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
port: 3000,
|
|
5
|
-
fetch: server,
|
|
6
|
-
}
|
package.json
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">= v20"
|
|
8
|
+
},
|
|
6
9
|
"dependencies": {
|
|
7
10
|
"history": "^5.3.0",
|
|
8
|
-
"radix3": "^1.0.0"
|
|
11
|
+
"radix3": "^1.0.0",
|
|
12
|
+
"isbot": "3.6.10"
|
|
9
13
|
},
|
|
10
14
|
"devDependencies": {
|
|
11
15
|
"autoprefixer": "^10.4.14",
|
pages/_404/page.css
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
body {
|
|
2
|
-
display: flex;
|
|
3
|
-
flex-direction: column;
|
|
4
|
-
align-items: center;
|
|
5
|
-
justify-content: center;
|
|
6
|
-
color: #000;
|
|
7
|
-
background: #fff;
|
|
8
|
-
font-family: -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Fira Sans", Avenir, "Helvetica Neue", "Lucida Grande", sans-serif;
|
|
9
|
-
height: 100vh;
|
|
10
|
-
text-align: center;
|
|
11
|
-
|
|
12
|
-
& h1 {
|
|
13
|
-
display: inline-block;
|
|
14
|
-
border-right: 1px solid rgba(0, 0, 0, .3);
|
|
15
|
-
margin: 0;
|
|
16
|
-
margin-right: 20px;
|
|
17
|
-
padding: 10px 23px 10px 0;
|
|
18
|
-
font-size: 24px;
|
|
19
|
-
font-weight: 500;
|
|
20
|
-
vertical-align: top;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
& .content {
|
|
24
|
-
display: inline-block;
|
|
25
|
-
text-align: left;
|
|
26
|
-
line-height: 49px;
|
|
27
|
-
height: 49px;
|
|
28
|
-
vertical-align: middle;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
& h2 {
|
|
32
|
-
font-size: 14px;
|
|
33
|
-
font-weight: normal;
|
|
34
|
-
line-height: inherit;
|
|
35
|
-
margin: 0;
|
|
36
|
-
padding: 0;
|
|
37
|
-
}
|
|
38
|
-
}
|
pages/_404/page.jsx
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Helmet } from 'react-helmet-async';
|
|
3
|
-
import "./page.css";
|
|
4
|
-
|
|
5
|
-
const Page = () => {
|
|
6
|
-
return (
|
|
7
|
-
<div>
|
|
8
|
-
<Helmet>
|
|
9
|
-
<title>Page not found</title>
|
|
10
|
-
</Helmet>
|
|
11
|
-
<h1>404 - Page not found</h1>
|
|
12
|
-
<div className="content">
|
|
13
|
-
<h2>This page could not be found</h2>
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default Page;
|
pages/_500/page.css
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
body {
|
|
2
|
-
display: flex;
|
|
3
|
-
flex-direction: column;
|
|
4
|
-
align-items: center;
|
|
5
|
-
justify-content: center;
|
|
6
|
-
color: #000;
|
|
7
|
-
background: #fff;
|
|
8
|
-
font-family: -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Fira Sans", Avenir, "Helvetica Neue", "Lucida Grande", sans-serif;
|
|
9
|
-
height: 100vh;
|
|
10
|
-
text-align: center;
|
|
11
|
-
|
|
12
|
-
& h1 {
|
|
13
|
-
display: inline-block;
|
|
14
|
-
border-right: 1px solid rgba(0, 0, 0, .3);
|
|
15
|
-
margin: 0;
|
|
16
|
-
margin-right: 20px;
|
|
17
|
-
padding: 10px 23px 10px 0;
|
|
18
|
-
font-size: 24px;
|
|
19
|
-
font-weight: 500;
|
|
20
|
-
vertical-align: top;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
& .content {
|
|
24
|
-
display: inline-block;
|
|
25
|
-
text-align: left;
|
|
26
|
-
line-height: 49px;
|
|
27
|
-
height: 49px;
|
|
28
|
-
vertical-align: middle;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
& h2 {
|
|
32
|
-
font-size: 14px;
|
|
33
|
-
font-weight: normal;
|
|
34
|
-
line-height: inherit;
|
|
35
|
-
margin: 0;
|
|
36
|
-
padding: 0;
|
|
37
|
-
}
|
|
38
|
-
}
|
pages/_500/page.jsx
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Helmet } from 'react-helmet-async';
|
|
3
|
-
import "./page.css";
|
|
4
|
-
|
|
5
|
-
const Page = () => {
|
|
6
|
-
return (
|
|
7
|
-
<div>
|
|
8
|
-
<Helmet>
|
|
9
|
-
<title>Oop's Something went wrong</title>
|
|
10
|
-
</Helmet>
|
|
11
|
-
<h1>Oop's Something went wrong</h1>
|
|
12
|
-
<div className="content">
|
|
13
|
-
<h2>Internal Server Error</h2>
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default Page;
|
pages/about/page.css
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
body {
|
|
2
|
-
margin: 0;
|
|
3
|
-
padding: 20px;
|
|
4
|
-
padding-bottom: 130px;
|
|
5
|
-
background-color: violet;
|
|
6
|
-
|
|
7
|
-
& footer {
|
|
8
|
-
margin-top: 100px;
|
|
9
|
-
}
|
|
10
|
-
}
|
pages/about/page.jsx
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Link, useRouter } from "parotta/runtime";
|
|
3
|
-
import { Helmet } from 'react-helmet-async';
|
|
4
|
-
import Layout from '@/components/Layout/Layout';
|
|
5
|
-
import "./page.css";
|
|
6
|
-
|
|
7
|
-
export const Page = () => {
|
|
8
|
-
const router = useRouter();
|
|
9
|
-
return (
|
|
10
|
-
<Layout>
|
|
11
|
-
<div className="about-page">
|
|
12
|
-
<Helmet>
|
|
13
|
-
<title>About Page @ {router.pathname}</title>
|
|
14
|
-
<meta name="description" content="Showcase of using parotta meta-framework." />
|
|
15
|
-
</Helmet>
|
|
16
|
-
<div>
|
|
17
|
-
<h1>About Page @ {router.pathname}</h1>
|
|
18
|
-
<p>Showcase of using parotta meta-framework.</p>
|
|
19
|
-
</div>
|
|
20
|
-
<footer>
|
|
21
|
-
<Link href="/">Back</Link>
|
|
22
|
-
</footer>
|
|
23
|
-
</div>
|
|
24
|
-
</Layout>
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export default Page;
|
pages/page.css
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
body {
|
|
2
|
-
margin: 0;
|
|
3
|
-
padding: 20px;
|
|
4
|
-
margin: 0;
|
|
5
|
-
background-color: turquoise;
|
|
6
|
-
|
|
7
|
-
& .count {
|
|
8
|
-
color: black;
|
|
9
|
-
padding: 40px;
|
|
10
|
-
font-size: 30px;
|
|
11
|
-
font-weight: 600;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
& footer {
|
|
15
|
-
margin-top: 100px;
|
|
16
|
-
}
|
|
17
|
-
}
|
pages/page.jsx
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
2
|
-
import { useRouter } from "parotta/runtime";
|
|
3
|
-
import Layout from '@/components/Layout/Layout';
|
|
4
|
-
import Counter from "@/components/Counter/Counter";
|
|
5
|
-
import { Helmet } from 'react-helmet-async';
|
|
6
|
-
import "./page.css";
|
|
7
|
-
|
|
8
|
-
const Page = () => {
|
|
9
|
-
const router = useRouter();
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
|
|
12
|
-
}, []);
|
|
13
|
-
return (
|
|
14
|
-
<Layout>
|
|
15
|
-
<Helmet>
|
|
16
|
-
<title>Parotta App</title>
|
|
17
|
-
</Helmet>
|
|
18
|
-
<div>
|
|
19
|
-
<h1>Home Page</h1>
|
|
20
|
-
<p>
|
|
21
|
-
Path: {router.pathname}
|
|
22
|
-
</p>
|
|
23
|
-
<Counter />
|
|
24
|
-
</div>
|
|
25
|
-
</Layout>
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export default Page;
|
pages/page.spec.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { test, expect } from '@playwright/test';
|
|
3
|
-
|
|
4
|
-
test.beforeEach(async ({ page }) => {
|
|
5
|
-
await page.goto('/');
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
test('has title', async ({ page }) => {
|
|
9
|
-
await expect(page).toHaveTitle(/Parotta/);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test('has links', async ({ page }) => {
|
|
13
|
-
// await page.getByRole('link', { name: 'About us' }).click();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('has counter', async ({ page }) => {
|
|
17
|
-
const counter = page.getByText("Counter");
|
|
18
|
-
expect(counter.innerText).toEqual("123");
|
|
19
|
-
});
|
pages/todos/page.css
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
body {
|
|
2
|
-
padding: 10px;
|
|
3
|
-
background-color: turquoise;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
:root {
|
|
8
|
-
--spectrum-alias-border-color: black;
|
|
9
|
-
--spectrum-global-color-gray-50: white;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.react-aria-TextField {
|
|
14
|
-
--field-border: var(--spectrum-alias-border-color);
|
|
15
|
-
--field-border-disabled: var(--spectrum-alias-border-color-disabled);
|
|
16
|
-
--field-background: var(--spectrum-global-color-gray-50);
|
|
17
|
-
--text-color: var(--spectrum-alias-text-color);
|
|
18
|
-
--text-color-disabled: var(--spectrum-alias-text-color-disabled);
|
|
19
|
-
--focus-ring-color: slateblue;
|
|
20
|
-
--invalid-color: var(--spectrum-global-color-red-600);
|
|
21
|
-
|
|
22
|
-
display: flex;
|
|
23
|
-
flex-direction: column;
|
|
24
|
-
width: fit-content;
|
|
25
|
-
|
|
26
|
-
.react-aria-Input {
|
|
27
|
-
padding: 0.286rem;
|
|
28
|
-
margin: 0;
|
|
29
|
-
border: 1px solid var(--field-border);
|
|
30
|
-
border-radius: 6px;
|
|
31
|
-
background: var(--field-background);
|
|
32
|
-
font-size: 1.143rem;
|
|
33
|
-
color: var(--text-color);
|
|
34
|
-
|
|
35
|
-
&[aria-invalid] {
|
|
36
|
-
border-color: var(--invalid-color);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
&:focus {
|
|
40
|
-
outline: none;
|
|
41
|
-
border-color: var(--focus-ring-color);
|
|
42
|
-
box-shadow: 0 0 0 1px var(--focus-ring-color);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
&:disabled {
|
|
46
|
-
border-color: var(--field-border-disabled);
|
|
47
|
-
color: var(--text-color-disabled);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
[slot=description] {
|
|
52
|
-
font-size: 12px;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
[slot=errorMessage] {
|
|
56
|
-
font-size: 12px;
|
|
57
|
-
color: var(--invalid-color);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
@media (forced-colors: active) {
|
|
62
|
-
.react-aria-TextField {
|
|
63
|
-
--field-border: ButtonBorder;
|
|
64
|
-
--field-border-disabled: GrayText;
|
|
65
|
-
--field-background: Field;
|
|
66
|
-
--text-color: FieldText;
|
|
67
|
-
--text-color-disabled: GrayText;
|
|
68
|
-
--focus-ring-color: Highlight;
|
|
69
|
-
--invalid-color: LinkText;
|
|
70
|
-
}
|
|
71
|
-
}
|
pages/todos/page.jsx
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import React, { Suspense } from 'react';
|
|
2
|
-
import { Helmet } from 'react-helmet-async';
|
|
3
|
-
import { useQuery, useMutation } from "parotta/runtime";
|
|
4
|
-
import { useForm } from 'react-hook-form';
|
|
5
|
-
import Todo from "@/components/Todo/Todo";
|
|
6
|
-
import { TextField, Label, Input } from 'react-aria-components';
|
|
7
|
-
import { Button } from 'react-aria-components';
|
|
8
|
-
import { getTodos, createTodo } from "@/services/todos.service";
|
|
9
|
-
import Layout from '@/components/Layout/Layout';
|
|
10
|
-
import "./page.css";
|
|
11
|
-
|
|
12
|
-
const TodoList = () => {
|
|
13
|
-
const { data, refetch } = useQuery("todos", () => getTodos());
|
|
14
|
-
const { mutate, isMutating } = useMutation(async ({ text }) => {
|
|
15
|
-
await createTodo({
|
|
16
|
-
text,
|
|
17
|
-
completed: false,
|
|
18
|
-
createdAt: new Date(),
|
|
19
|
-
})
|
|
20
|
-
await refetch();
|
|
21
|
-
});
|
|
22
|
-
const { register, handleSubmit, formState: { errors } } = useForm();
|
|
23
|
-
return (
|
|
24
|
-
<div>
|
|
25
|
-
<ul>
|
|
26
|
-
{data.map((item) => (
|
|
27
|
-
<Todo key={item.id} todo={item} />
|
|
28
|
-
))}
|
|
29
|
-
</ul>
|
|
30
|
-
<form onSubmit={handleSubmit(mutate)}>
|
|
31
|
-
<TextField isRequired isReadOnly={isMutating}>
|
|
32
|
-
<Label>Text (required)</Label>
|
|
33
|
-
<Input {...register('text', { required: true })} />
|
|
34
|
-
{errors.text && <p>Please enter some text</p>}
|
|
35
|
-
</TextField>
|
|
36
|
-
<Button type="submit" isDisabled={isMutating}>Add Todo</Button>
|
|
37
|
-
{isMutating && <div>
|
|
38
|
-
<p>Creating...</p>
|
|
39
|
-
</div>}
|
|
40
|
-
</form>
|
|
41
|
-
</div>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const Page = () => {
|
|
46
|
-
return (
|
|
47
|
-
<Layout>
|
|
48
|
-
<h1>Todos</h1>
|
|
49
|
-
<Helmet>
|
|
50
|
-
<title>Todos Page</title>
|
|
51
|
-
</Helmet>
|
|
52
|
-
<div>
|
|
53
|
-
<Suspense fallback="Loading...">
|
|
54
|
-
<TodoList />
|
|
55
|
-
</Suspense>
|
|
56
|
-
</div>
|
|
57
|
-
</Layout>
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export default Page;
|
parotta/bun.lockb
DELETED
|
Binary file
|
parotta/package.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "parotta",
|
|
3
|
-
"version": "0.5.0",
|
|
4
|
-
"type": "module",
|
|
5
|
-
"dependencies": {
|
|
6
|
-
"autoprefixer": "^10.4.14",
|
|
7
|
-
"history": "^5.3.0",
|
|
8
|
-
"mime-types": "2.1.35",
|
|
9
|
-
"postcss": "^8.4.21",
|
|
10
|
-
"postcss-custom-media": "^9.1.2",
|
|
11
|
-
"postcss-nesting": "^11.2.1",
|
|
12
|
-
"radix3": "^1.0.0",
|
|
13
|
-
"walkdir": "0.4.1"
|
|
14
|
-
},
|
|
15
|
-
"prettier": {
|
|
16
|
-
"printWidth": 120
|
|
17
|
-
}
|
|
18
|
-
}
|
parotta/readme.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# parotta
|
|
2
|
-
|
|
3
|
-
parotta is a next level meta-framework for react that runs only on edge runtimes.
|
|
4
|
-
It uses bun as its bundler/transpiler and development mode as its quick and fast.
|
|
5
|
-
It uses File System routing with streaming SSR + CSR as the method to render pages. Basically a MPA + SPA Transitional App.
|
|
6
|
-
It is very opionated and has set of idiomatic ways of doing things.
|
|
7
|
-
It has inbuilt rpc mechanism to access server resources instead of a typical REST API.
|
|
8
|
-
|
|
9
|
-
### Todo
|
|
10
|
-
1. Add build step
|
|
11
|
-
2. Deploy to Node (using edge-runtime), Docker, Deno deploy, Vercel edge functions, Cloudflare workers, Bun edge (whenever it releases)
|
|
12
|
-
3. Hydrate rpc cache
|
|
13
|
-
4. Build a Website with Docs using parotta
|
parotta/runtime.js
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
Suspense, createElement, createContext, useContext, useState, useEffect, useTransition, useCallback
|
|
3
|
-
} from "react";
|
|
4
|
-
import { HelmetProvider } from 'react-helmet-async';
|
|
5
|
-
import { ErrorBoundary } from "react-error-boundary";
|
|
6
|
-
|
|
7
|
-
export const domain = () => typeof window !== 'undefined' ? window.origin : "http://0.0.0.0:3000";
|
|
8
|
-
|
|
9
|
-
export const rpc = (serviceName) => async (params = {}) => {
|
|
10
|
-
const res = await fetch(`${domain()}/services/${serviceName}`, {
|
|
11
|
-
method: "POST",
|
|
12
|
-
headers: {
|
|
13
|
-
"Accept": "application/json",
|
|
14
|
-
"Content-Type": "application/json",
|
|
15
|
-
},
|
|
16
|
-
body: JSON.stringify(params),
|
|
17
|
-
})
|
|
18
|
-
return await res.json();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const RpcContext = createContext(undefined);
|
|
22
|
-
|
|
23
|
-
// global way to refresh maybe without being tied to a hook like refetch
|
|
24
|
-
export const useInvalidate = () => {
|
|
25
|
-
const ctx = useContext(RpcContext);
|
|
26
|
-
return (regex) => {
|
|
27
|
-
Object.keys(ctx)
|
|
28
|
-
.filter((k) => regex.test(k))
|
|
29
|
-
.forEach((k) => {
|
|
30
|
-
delete ctx[k];
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const useRpcCache = (k) => {
|
|
36
|
-
const ctx = useContext(RpcContext);
|
|
37
|
-
const [_, rerender] = useState(false);
|
|
38
|
-
const get = () => ctx[k]
|
|
39
|
-
const set = (v) => {
|
|
40
|
-
ctx[k] = v;
|
|
41
|
-
rerender((c) => !c);
|
|
42
|
-
}
|
|
43
|
-
const invalidate = () => {
|
|
44
|
-
delete ctx[k];
|
|
45
|
-
rerender((c) => !c);
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
get,
|
|
49
|
-
set,
|
|
50
|
-
invalidate,
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
*
|
|
56
|
-
* @param {*} fn
|
|
57
|
-
* @param {*} params
|
|
58
|
-
* @returns
|
|
59
|
-
*/
|
|
60
|
-
export const useQuery = (key, fn) => {
|
|
61
|
-
const [isRefetching, setIsRefetching] = useState(false);
|
|
62
|
-
const [err, setErr] = useState(null);
|
|
63
|
-
const cache = useRpcCache(key);
|
|
64
|
-
const refetch = useCallback(async () => {
|
|
65
|
-
try {
|
|
66
|
-
setIsRefetching(true);
|
|
67
|
-
setErr(null);
|
|
68
|
-
cache.set(await fn());
|
|
69
|
-
} catch (err) {
|
|
70
|
-
setErr(err);
|
|
71
|
-
throw err;
|
|
72
|
-
} finally {
|
|
73
|
-
setIsRefetching(false);
|
|
74
|
-
}
|
|
75
|
-
}, [fn]);
|
|
76
|
-
const value = cache.get();
|
|
77
|
-
if (value) {
|
|
78
|
-
if (value instanceof Promise) {
|
|
79
|
-
throw value;
|
|
80
|
-
} else if (value instanceof Error) {
|
|
81
|
-
throw value;
|
|
82
|
-
}
|
|
83
|
-
return { data: value, isRefetching, err, refetch };
|
|
84
|
-
}
|
|
85
|
-
cache.set(fn().then((v) => cache.set(v)));
|
|
86
|
-
throw cache.get();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export const useMutation = (fn) => {
|
|
90
|
-
const [isMutating, setIsMutating] = useState(false);
|
|
91
|
-
const [err, setErr] = useState(null);
|
|
92
|
-
const mutate = useCallback(async (params) => {
|
|
93
|
-
try {
|
|
94
|
-
setIsMutating(true);
|
|
95
|
-
setErr(null);
|
|
96
|
-
await fn(params);
|
|
97
|
-
} catch (err) {
|
|
98
|
-
setErr(err)
|
|
99
|
-
throw err;
|
|
100
|
-
} finally {
|
|
101
|
-
setIsMutating(false);
|
|
102
|
-
}
|
|
103
|
-
}, [fn])
|
|
104
|
-
return {
|
|
105
|
-
mutate,
|
|
106
|
-
isMutating,
|
|
107
|
-
err,
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export const RouterContext = createContext(undefined);
|
|
112
|
-
|
|
113
|
-
const getMatch = (radixRouter, pathname) => {
|
|
114
|
-
const matchedPage = radixRouter.lookup(pathname);
|
|
115
|
-
if (!matchedPage) {
|
|
116
|
-
return React.lazy(() => import("/pages/_404/page.jsx"));
|
|
117
|
-
}
|
|
118
|
-
return matchedPage;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const getCssUrl = (pathname) => `/pages${pathname === "/" ? "" : pathname}/page.css`;
|
|
122
|
-
|
|
123
|
-
export const App = ({ nProgress, history, radixRouter, rpcCache, helmetContext }) => {
|
|
124
|
-
const [isPending, startTransition] = useTransition();
|
|
125
|
-
const [match, setMatch] = useState(() => getMatch(radixRouter, history.location.pathname));
|
|
126
|
-
useEffect(() => {
|
|
127
|
-
return history.listen(({ location }) => {
|
|
128
|
-
const href = getCssUrl(location.pathname);
|
|
129
|
-
// const isLoaded = Array.from(document.getElementsByTagName("link"))
|
|
130
|
-
// .map((link) => link.href.replace(window.origin, "")).includes(href);
|
|
131
|
-
// if (!isLoaded) {
|
|
132
|
-
// const link = document.createElement('link');
|
|
133
|
-
// link.setAttribute("rel", "stylesheet");
|
|
134
|
-
// link.setAttribute("type", "text/css");
|
|
135
|
-
// link.onload = () => {
|
|
136
|
-
// nProgress.start();
|
|
137
|
-
// startTransition(() => {
|
|
138
|
-
// setMatch(getMatch(radixRouter, location.pathname));
|
|
139
|
-
// })
|
|
140
|
-
// };
|
|
141
|
-
// link.setAttribute("href", href);
|
|
142
|
-
// document.getElementsByTagName("head")[0].appendChild(link);
|
|
143
|
-
// } else {
|
|
144
|
-
const link = document.createElement('link');
|
|
145
|
-
link.setAttribute("rel", "stylesheet");
|
|
146
|
-
link.setAttribute("type", "text/css");
|
|
147
|
-
link.setAttribute("href", href);
|
|
148
|
-
document.getElementsByTagName("head")[0].appendChild(link);
|
|
149
|
-
nProgress.start();
|
|
150
|
-
startTransition(() => {
|
|
151
|
-
setMatch(getMatch(radixRouter, location.pathname));
|
|
152
|
-
})
|
|
153
|
-
// }
|
|
154
|
-
});
|
|
155
|
-
}, []);
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
if (!isPending) {
|
|
158
|
-
nProgress.done();
|
|
159
|
-
}
|
|
160
|
-
}, [isPending]);
|
|
161
|
-
return createElement(HelmetProvider, {
|
|
162
|
-
context: helmetContext,
|
|
163
|
-
children: createElement(RpcContext.Provider, {
|
|
164
|
-
value: rpcCache,
|
|
165
|
-
children: createElement(RouterContext.Provider, {
|
|
166
|
-
value: {
|
|
167
|
-
history: history,
|
|
168
|
-
params: match.params || {},
|
|
169
|
-
},
|
|
170
|
-
children: createElement(ErrorBoundary, {
|
|
171
|
-
onError: (err) => console.log(err),
|
|
172
|
-
fallback: createElement("p", {}, "Oops something went wrong"),
|
|
173
|
-
children: createElement(Suspense, {
|
|
174
|
-
fallback: createElement("p", {}, "Loading..."),
|
|
175
|
-
children: createElement(match, {}),
|
|
176
|
-
}),
|
|
177
|
-
}),
|
|
178
|
-
}),
|
|
179
|
-
}),
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export const useRouter = () => {
|
|
184
|
-
const { history, params } = useContext(RouterContext);
|
|
185
|
-
return {
|
|
186
|
-
pathname: history.location.pathname,
|
|
187
|
-
query: new URLSearchParams(history.location.search),
|
|
188
|
-
params,
|
|
189
|
-
push: history.push,
|
|
190
|
-
replace: history.replace,
|
|
191
|
-
forward: history.forward,
|
|
192
|
-
back: history.back,
|
|
193
|
-
reload: () => window.location.reload(),
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export const Link = (props) => {
|
|
198
|
-
const router = useRouter();
|
|
199
|
-
return createElement("a", {
|
|
200
|
-
...props,
|
|
201
|
-
onMouseOver: (e) => {
|
|
202
|
-
// Simple prefetching for now will work only with cache headers
|
|
203
|
-
// fetch(getCssUrl(props.href));
|
|
204
|
-
// fetch(getCssUrl(props.href).replace("css", "jsx"));
|
|
205
|
-
},
|
|
206
|
-
onClick: (e) => {
|
|
207
|
-
e.preventDefault();
|
|
208
|
-
if (props && props.onClick) {
|
|
209
|
-
props.onClick(e);
|
|
210
|
-
}
|
|
211
|
-
router.push(props.href)
|
|
212
|
-
},
|
|
213
|
-
})
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export const NavLink = ({ children, className, activeClassName, ...props }) => {
|
|
217
|
-
const { pathname } = useRouter();
|
|
218
|
-
const classNames = pathname === props.href ? [activeClassName, className] : [className];
|
|
219
|
-
return createElement(Link, {
|
|
220
|
-
children,
|
|
221
|
-
className: classNames,
|
|
222
|
-
...props,
|
|
223
|
-
})
|
|
224
|
-
}
|
parotta/server.js
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { renderToReadableStream } from "react-dom/server";
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import walkdir from 'walkdir';
|
|
5
|
-
import postcss from "postcss"
|
|
6
|
-
import autoprefixer from "autoprefixer";
|
|
7
|
-
import postcssCustomMedia from "postcss-custom-media";
|
|
8
|
-
import postcssNesting from "postcss-nesting";
|
|
9
|
-
import { createMemoryHistory } from "history";
|
|
10
|
-
import { createRouter } from 'radix3';
|
|
11
|
-
import mimeTypes from "mime-types";
|
|
12
|
-
import { App } from "./runtime";
|
|
13
|
-
|
|
14
|
-
if (!globalThis.firstRun) {
|
|
15
|
-
globalThis.firstRun = true
|
|
16
|
-
const version = (await import(path.join(import.meta.dir, "package.json"))).default.version;
|
|
17
|
-
console.log(`parotta v${version}`)
|
|
18
|
-
console.log(`running with cwd=${path.basename(process.cwd())} node_env=${process.env.NODE_ENV}`);
|
|
19
|
-
} else {
|
|
20
|
-
console.log(`server reloading`);
|
|
21
|
-
}
|
|
22
|
-
const isProd = process.env.NODE_ENV === "production";
|
|
23
|
-
|
|
24
|
-
const createServerRouter = async () => {
|
|
25
|
-
const routes = {};
|
|
26
|
-
const dirs = walkdir.sync(path.join(process.cwd(), "pages"))
|
|
27
|
-
.map((s) => s.replace(process.cwd(), "")
|
|
28
|
-
.replace("/pages", "")
|
|
29
|
-
// .replaceAll("[", ":")
|
|
30
|
-
// .replaceAll("]", "")
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
walkdir.sync(path.join(process.cwd(), "services"))
|
|
34
|
-
.map((s) => s.replace(process.cwd(), ""))
|
|
35
|
-
.filter((s) => s.includes(".service.js"))
|
|
36
|
-
.forEach((s) => {
|
|
37
|
-
const serviceName = s.replace(".service.js", "");
|
|
38
|
-
routes[serviceName + "/*"] = { key: serviceName, service: s };
|
|
39
|
-
});
|
|
40
|
-
dirs.filter((p) => p.includes('page.jsx'))
|
|
41
|
-
.map((s) => ({ path: s, route: s.replace("/page.jsx", "") }))
|
|
42
|
-
.forEach((page) => {
|
|
43
|
-
const key = page.route || "/";
|
|
44
|
-
routes[key] = { key: key, page: page.path };
|
|
45
|
-
});
|
|
46
|
-
walkdir.sync(path.join(process.cwd(), "static"))
|
|
47
|
-
.map((s) => s.replace(process.cwd(), "").replace("/static", ""))
|
|
48
|
-
.forEach((route) => {
|
|
49
|
-
routes[route] = { key: route, file: route }
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
return createRouter({
|
|
53
|
-
strictTrailingSlash: true,
|
|
54
|
-
routes: routes,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const createClientRouter = async () => {
|
|
59
|
-
const routes = await walkdir.sync(path.join(process.cwd(), "pages"))
|
|
60
|
-
.filter((p) => p.includes("page.jsx"))
|
|
61
|
-
.filter((p) => !p.includes("/_"))
|
|
62
|
-
.map((s) => s.replace(process.cwd(), ""))
|
|
63
|
-
.map((s) => s.replace("/pages", ""))
|
|
64
|
-
.map((s) => s.replace("/page.jsx", ""))
|
|
65
|
-
.reduce(async (accp, r) => {
|
|
66
|
-
const acc = await accp;
|
|
67
|
-
const src = await import(`${process.cwd()}/pages${r}/page.jsx`);
|
|
68
|
-
acc[r === "" ? "/" : r] = src.default;
|
|
69
|
-
return acc
|
|
70
|
-
}, Promise.resolve({}));
|
|
71
|
-
// console.log(clientRoutes);
|
|
72
|
-
const hydrationScript = `
|
|
73
|
-
import React from "react";
|
|
74
|
-
import { hydrateRoot } from "react-dom/client";
|
|
75
|
-
import { createBrowserHistory } from "history";
|
|
76
|
-
import nProgress from "nprogress";
|
|
77
|
-
import { createRouter } from "radix3";
|
|
78
|
-
import { App } from "parotta/runtime";
|
|
79
|
-
|
|
80
|
-
const history = createBrowserHistory();
|
|
81
|
-
const radixRouter = createRouter({
|
|
82
|
-
strictTrailingSlash: true,
|
|
83
|
-
routes: {
|
|
84
|
-
${Object.keys(routes).map((r) => `"${r}": React.lazy(() => import("/pages${r}/page.jsx"))`).join(',\n ')}
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
hydrateRoot(document.body, React.createElement(App, {
|
|
89
|
-
nProgress,
|
|
90
|
-
history,
|
|
91
|
-
radixRouter,
|
|
92
|
-
rpcCache: {},
|
|
93
|
-
helmetContext: {},
|
|
94
|
-
}));`
|
|
95
|
-
const router = createRouter({
|
|
96
|
-
strictTrailingSlash: true,
|
|
97
|
-
routes: routes,
|
|
98
|
-
});
|
|
99
|
-
router.hydrationScript = hydrationScript;
|
|
100
|
-
return router;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const mapDeps = (dir) => {
|
|
104
|
-
return walkdir.sync(path.join(process.cwd(), dir))
|
|
105
|
-
.map((s) => s.replace(process.cwd(), ""))
|
|
106
|
-
.filter((s) => s.includes(".jsx") || s.includes(".js"))
|
|
107
|
-
.reduce((acc, s) => {
|
|
108
|
-
if (s.includes(".jsx")) {
|
|
109
|
-
acc['@' + s.replace(".jsx", "")] = s
|
|
110
|
-
}
|
|
111
|
-
if (s.includes(".js")) {
|
|
112
|
-
acc['@' + s.replace(".js", "")] = s
|
|
113
|
-
}
|
|
114
|
-
return acc;
|
|
115
|
-
}, {});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const serverRouter = await createServerRouter();
|
|
119
|
-
const clientRouter = await createClientRouter();
|
|
120
|
-
const transpiler = new Bun.Transpiler({
|
|
121
|
-
loader: "jsx",
|
|
122
|
-
autoImportJSX: true,
|
|
123
|
-
jsxOptimizationInline: true,
|
|
124
|
-
|
|
125
|
-
// TODO
|
|
126
|
-
// autoImportJSX: false,
|
|
127
|
-
// jsxOptimizationInline: false,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const renderApi = async (key, filePath, req) => {
|
|
131
|
-
const url = new URL(req.url);
|
|
132
|
-
const params = req.method === "POST" ? await req.json() : Object.fromEntries(url.searchParams);
|
|
133
|
-
const funcName = url.pathname.replace(`${key}/`, "");
|
|
134
|
-
const js = await import(path.join(process.cwd(), filePath));
|
|
135
|
-
const result = await js[funcName](params);
|
|
136
|
-
return new Response(JSON.stringify(result), {
|
|
137
|
-
headers: { 'Content-Type': 'application/json' },
|
|
138
|
-
status: 200,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const renderPage = async (url) => {
|
|
143
|
-
const packageJson = await import(path.join(process.cwd(), "package.json"));
|
|
144
|
-
const config = packageJson.default.parotta || { hydrate: true };
|
|
145
|
-
const devTag = !isProd ? "?dev" : "";
|
|
146
|
-
const nodeDeps = Object.keys(packageJson.default.dependencies).reduce((acc, dep) => {
|
|
147
|
-
acc[dep] = `https://esm.sh/${dep}@${packageJson.default.dependencies[dep]}`;
|
|
148
|
-
return acc;
|
|
149
|
-
}, {})
|
|
150
|
-
const components = mapDeps("components");
|
|
151
|
-
const importMap = {
|
|
152
|
-
"radix3": `https://esm.sh/radix3@1.0.1`,
|
|
153
|
-
"history": "https://esm.sh/history@5.3.0",
|
|
154
|
-
"react": `https://esm.sh/react@18.2.0${devTag}`,
|
|
155
|
-
// TODO: need to remove this in prod
|
|
156
|
-
"react/jsx-dev-runtime": `https://esm.sh/react@18.2.0${devTag}/jsx-dev-runtime`,
|
|
157
|
-
"react-dom/client": `https://esm.sh/react-dom@18.2.0${devTag}/client`,
|
|
158
|
-
"nprogress": "https://esm.sh/nprogress@0.2.0",
|
|
159
|
-
// "parotta/runtime": `https://esm.sh/parotta@${version}/runtime.js`,
|
|
160
|
-
"parotta/runtime": `/parotta/runtime.js`,
|
|
161
|
-
...nodeDeps,
|
|
162
|
-
...components,
|
|
163
|
-
};
|
|
164
|
-
const history = createMemoryHistory({
|
|
165
|
-
initialEntries: [url.pathname + url.search],
|
|
166
|
-
});
|
|
167
|
-
const helmetContext = {}
|
|
168
|
-
const nProgress = { start: () => { }, done: () => { } }
|
|
169
|
-
const stream = await renderToReadableStream(
|
|
170
|
-
<html lang="en">
|
|
171
|
-
<head>
|
|
172
|
-
<link rel="stylesheet" href="https://unpkg.com/nprogress@0.2.0/nprogress.css" />
|
|
173
|
-
<link id="pageCss" rel="stylesheet" href={`/pages${url.pathname}/page.css`} />
|
|
174
|
-
<script type="importmap" dangerouslySetInnerHTML={{ __html: JSON.stringify({ "imports": importMap }) }} />
|
|
175
|
-
</head>
|
|
176
|
-
<body>
|
|
177
|
-
<App
|
|
178
|
-
nProgress={nProgress}
|
|
179
|
-
history={history}
|
|
180
|
-
radixRouter={clientRouter}
|
|
181
|
-
rpcCache={{}}
|
|
182
|
-
helmetContext={helmetContext}
|
|
183
|
-
/>
|
|
184
|
-
{config.hydrate &&
|
|
185
|
-
<>
|
|
186
|
-
<script type="module" defer={true} dangerouslySetInnerHTML={{
|
|
187
|
-
__html: clientRouter.hydrationScript
|
|
188
|
-
}}>
|
|
189
|
-
</script>
|
|
190
|
-
</>
|
|
191
|
-
}
|
|
192
|
-
</body>
|
|
193
|
-
</html >
|
|
194
|
-
);
|
|
195
|
-
// TODO:
|
|
196
|
-
// if (bot || isCrawler) {
|
|
197
|
-
// await stream.allReady
|
|
198
|
-
// add helmetContext to head
|
|
199
|
-
// }
|
|
200
|
-
return new Response(stream, {
|
|
201
|
-
headers: { 'Content-Type': 'text/html' },
|
|
202
|
-
status: 200,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const renderCss = async (src) => {
|
|
207
|
-
try {
|
|
208
|
-
const cssText = await Bun.file(src).text();
|
|
209
|
-
const result = await postcss([
|
|
210
|
-
autoprefixer(),
|
|
211
|
-
postcssCustomMedia(),
|
|
212
|
-
// postcssNormalize({ browsers: 'last 2 versions' }),
|
|
213
|
-
postcssNesting,
|
|
214
|
-
]).process(cssText, { from: src, to: src });
|
|
215
|
-
return new Response(result.css, {
|
|
216
|
-
headers: { 'Content-Type': 'text/css' },
|
|
217
|
-
status: 200,
|
|
218
|
-
});
|
|
219
|
-
} catch (err) {
|
|
220
|
-
return new Response(`Not Found`, {
|
|
221
|
-
headers: { 'Content-Type': 'text/html' },
|
|
222
|
-
status: 404,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const renderJs = async (srcFile) => {
|
|
228
|
-
try {
|
|
229
|
-
const jsText = await Bun.file(srcFile).text();
|
|
230
|
-
const result = await transpiler.transform(jsText);
|
|
231
|
-
// inject code which calls the api for that function
|
|
232
|
-
const lines = result.split("\n");
|
|
233
|
-
// lines.unshift(`import React from "react";`);
|
|
234
|
-
|
|
235
|
-
// replace all .service imports which rpc interface
|
|
236
|
-
let addRpcImport = false;
|
|
237
|
-
lines.forEach((ln) => {
|
|
238
|
-
if (ln.includes(".service")) {
|
|
239
|
-
addRpcImport = true;
|
|
240
|
-
const [importName, serviceName] = ln.match(/\@\/services\/(.*)\.service/);
|
|
241
|
-
const funcsText = ln.replace(`from "${importName}"`, "").replace("import", "").replace("{", "").replace("}", "").replace(";", "");
|
|
242
|
-
const funcsName = funcsText.split(",");
|
|
243
|
-
funcsName.forEach((fnName) => {
|
|
244
|
-
lines.push(`const ${fnName} = rpc("${serviceName}/${fnName.trim()}")`);
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
})
|
|
248
|
-
if (addRpcImport) {
|
|
249
|
-
lines.unshift(`import { rpc } from "parotta/runtime";`);
|
|
250
|
-
}
|
|
251
|
-
// remove .css and .service imports
|
|
252
|
-
const filteredJsx = lines.filter((ln) => !ln.includes(`.css"`) && !ln.includes(`.service"`)).join("\n");
|
|
253
|
-
//.replaceAll("$jsx", "React.createElement");
|
|
254
|
-
return new Response(filteredJsx, {
|
|
255
|
-
headers: {
|
|
256
|
-
'Content-Type': 'application/javascript',
|
|
257
|
-
},
|
|
258
|
-
status: 200,
|
|
259
|
-
});
|
|
260
|
-
} catch (err) {
|
|
261
|
-
return new Response(`Not Found`, {
|
|
262
|
-
headers: { 'Content-Type': 'text/html' },
|
|
263
|
-
status: 404,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const sendFile = async (src) => {
|
|
269
|
-
try {
|
|
270
|
-
const contentType = mimeTypes.lookup(src) || "application/octet-stream";
|
|
271
|
-
const stream = await Bun.file(src).stream();
|
|
272
|
-
return new Response(stream, {
|
|
273
|
-
headers: { 'Content-Type': contentType },
|
|
274
|
-
status: 200,
|
|
275
|
-
});
|
|
276
|
-
} catch (err) {
|
|
277
|
-
return new Response(`Not Found`, {
|
|
278
|
-
headers: { 'Content-Type': 'text/html' },
|
|
279
|
-
status: 404,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const server = async (req) => {
|
|
285
|
-
const url = new URL(req.url);
|
|
286
|
-
console.log(req.method, url.pathname);
|
|
287
|
-
// maybe this is needed
|
|
288
|
-
if (url.pathname.startsWith("/parotta/")) {
|
|
289
|
-
return renderJs(path.join(import.meta.dir, url.pathname.replace("/parotta/", "")));
|
|
290
|
-
}
|
|
291
|
-
if (url.pathname.endsWith(".css")) {
|
|
292
|
-
return renderCss(path.join(process.cwd(), url.pathname));
|
|
293
|
-
}
|
|
294
|
-
if (url.pathname.endsWith(".js") || url.pathname.endsWith(".jsx")) {
|
|
295
|
-
return renderJs(path.join(process.cwd(), url.pathname));
|
|
296
|
-
}
|
|
297
|
-
const match = serverRouter.lookup(url.pathname);
|
|
298
|
-
if (match && !match.key.includes("/_")) {
|
|
299
|
-
if (match.file) {
|
|
300
|
-
return sendFile(path.join(process.cwd(), `/static${match.file}`));
|
|
301
|
-
}
|
|
302
|
-
if (match.page && req.headers.get("Accept")?.includes('text/html')) {
|
|
303
|
-
return renderPage(url);
|
|
304
|
-
}
|
|
305
|
-
if (match.service) {
|
|
306
|
-
return renderApi(match.key, match.service, req);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if (req.headers.get("Accept")?.includes('text/html')) {
|
|
310
|
-
// not found html page
|
|
311
|
-
return renderPage(new URL(`${url.protocol}//${url.host}/_404`));
|
|
312
|
-
}
|
|
313
|
-
// not found generic page
|
|
314
|
-
return new Response(`{"message": "not found"}`, {
|
|
315
|
-
headers: { 'Content-Type': 'application/json' },
|
|
316
|
-
status: 404,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export default server;
|
playwright.config.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { defineConfig, devices } from '@playwright/test';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @see https://playwright.dev/docs/test-configuration
|
|
6
|
-
*/
|
|
7
|
-
export default defineConfig({
|
|
8
|
-
testDir: './routes',
|
|
9
|
-
timeout: 30 * 1000,
|
|
10
|
-
expect: {
|
|
11
|
-
timeout: 5000
|
|
12
|
-
},
|
|
13
|
-
fullyParallel: true,
|
|
14
|
-
forbidOnly: !!process.env.CI,
|
|
15
|
-
retries: process.env.CI ? 2 : 0,
|
|
16
|
-
workers: process.env.CI ? 1 : undefined,
|
|
17
|
-
reporter: 'html',
|
|
18
|
-
use: {
|
|
19
|
-
actionTimeout: 0,
|
|
20
|
-
baseURL: 'http://localhost:3000',
|
|
21
|
-
trace: 'on-first-retry',
|
|
22
|
-
},
|
|
23
|
-
projects: [
|
|
24
|
-
{
|
|
25
|
-
name: 'chromium',
|
|
26
|
-
use: { ...devices['Desktop Chrome'] },
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
name: 'firefox',
|
|
31
|
-
use: { ...devices['Desktop Firefox'] },
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
name: 'webkit',
|
|
36
|
-
use: { ...devices['Desktop Safari'] },
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
/* Test against mobile viewports. */
|
|
40
|
-
// {
|
|
41
|
-
// name: 'Mobile Chrome',
|
|
42
|
-
// use: { ...devices['Pixel 5'] },
|
|
43
|
-
// },
|
|
44
|
-
// {
|
|
45
|
-
// name: 'Mobile Safari',
|
|
46
|
-
// use: { ...devices['iPhone 12'] },
|
|
47
|
-
// },
|
|
48
|
-
|
|
49
|
-
/* Test against branded browsers. */
|
|
50
|
-
// {
|
|
51
|
-
// name: 'Microsoft Edge',
|
|
52
|
-
// use: { channel: 'msedge' },
|
|
53
|
-
// },
|
|
54
|
-
// {
|
|
55
|
-
// name: 'Google Chrome',
|
|
56
|
-
// use: { channel: 'chrome' },
|
|
57
|
-
// },
|
|
58
|
-
],
|
|
59
|
-
outputDir: 'test-results/',
|
|
60
|
-
webServer: {
|
|
61
|
-
command: '../parotta/cli.js',
|
|
62
|
-
port: 3000,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
pnpm-lock.yaml
CHANGED
|
@@ -7,6 +7,9 @@ importers:
|
|
|
7
7
|
history:
|
|
8
8
|
specifier: ^5.3.0
|
|
9
9
|
version: 5.3.0
|
|
10
|
+
isbot:
|
|
11
|
+
specifier: 3.6.10
|
|
12
|
+
version: 3.6.10
|
|
10
13
|
nprogress:
|
|
11
14
|
specifier: '*'
|
|
12
15
|
version: 0.2.0
|
|
@@ -4776,6 +4779,11 @@ packages:
|
|
|
4776
4779
|
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
|
4777
4780
|
dev: true
|
|
4778
4781
|
|
|
4782
|
+
/isbot@3.6.10:
|
|
4783
|
+
resolution: {integrity: sha512-+I+2998oyP4oW9+OTQD8TS1r9P6wv10yejukj+Ksj3+UR5pUhsZN3f8W7ysq0p1qxpOVNbl5mCuv0bCaF8y5iQ==}
|
|
4784
|
+
engines: {node: '>=12'}
|
|
4785
|
+
dev: false
|
|
4786
|
+
|
|
4779
4787
|
/isexe@2.0.0:
|
|
4780
4788
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
|
4781
4789
|
dev: true
|
readme.md
CHANGED
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
edge-city is a next level meta-framework for react that runs only on edge runtimes.
|
|
4
4
|
It uses esbuild as its bundler/transpiler.
|
|
5
|
-
It uses file system routing (similar to
|
|
5
|
+
It uses file system routing (similar to nextjs app router) with streaming SSR + CSR to render pages.
|
|
6
6
|
It is very opionated and has set of idiomatic ways of doing things.
|
|
7
7
|
It has an inbuilt rpc mechanism to access server resources instead of a typical REST API.
|
|
8
8
|
It aims to have almost the same router api as nextjs router for ease of use.
|
|
9
9
|
|
|
10
|
-
During development each request for a page is executed in a separate edge-runtime (miniflare/vercel) vm.
|
|
10
|
+
During development each request for a page is executed in a separate edge-runtime (miniflare/vercel) vm.
|
|
11
11
|
During production each page is packaged to an esm function adapted to the platform of your choice.
|
|
12
12
|
|
|
13
13
|
## Why?
|
|
14
|
+
Beacause,
|
|
14
|
-
|
|
15
|
+
* Its really hard to have a streaming SSR + CSR setup in nextjs currently.
|
|
15
|
-
|
|
16
|
+
* There is no framework which runs your code in an edge simulated environment during development and targets only edge for production.
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
1. `node >= v20`
|
|
20
|
+
2. `wrangler` for deploying to cloudflare page functions
|
|
21
|
+
3. `vercel` for deploying to vercel edge runtime
|
|
16
22
|
|
|
17
23
|
### Supported platforms
|
|
18
24
|
1. [Cloudflare page functions](https://developers.cloudflare.com/pages/platform/functions/routing/)
|
|
@@ -20,8 +26,16 @@ The only other framework is rakkasjs but it doesn't maitaing as smooth transitio
|
|
|
20
26
|
3. [Netlify edge functions](https://docs.netlify.com/edge-functions/overview/)
|
|
21
27
|
4. [Deno Deploy](https://deno.com/deploy)
|
|
22
28
|
|
|
29
|
+
## Developing
|
|
30
|
+
|
|
31
|
+
1. `node >= v20.2.0`
|
|
32
|
+
2. `pnpm >= v8.5.1`
|
|
33
|
+
|
|
23
34
|
### Todo
|
|
24
35
|
1. Hydrate rpc cache
|
|
25
36
|
2. Build a docs website
|
|
26
37
|
3. Fix 404/500 pages not routing
|
|
27
|
-
|
|
38
|
+
4. Add Env variables `PUBLIC_` for client
|
|
39
|
+
5. Add tests for bot
|
|
40
|
+
6. Add tests for runtime
|
|
41
|
+
7. Maybe move to vite for HMR goodness
|
services/auth.service.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// import NextAuth from "next-auth";
|
|
2
|
-
// import EmailProvider from "next-auth/providers/email";
|
|
3
|
-
// import GoogleProvider from "next-auth/providers/google";
|
|
4
|
-
// import DrizzleAuthAdapterPG from "drizzle-auth-adaptor-pg";
|
|
5
|
-
// import db from "@/db";
|
|
6
|
-
|
|
7
|
-
// GET /api/auth/signin
|
|
8
|
-
// POST /api/auth/signin/:provider
|
|
9
|
-
// GET/POST /api/auth/callback/:provider
|
|
10
|
-
// GET /api/auth/signout
|
|
11
|
-
// POST /api/auth/signout
|
|
12
|
-
// GET /api/auth/session
|
|
13
|
-
// GET /api/auth/csrf
|
|
14
|
-
// GET /api/auth/providers
|
|
15
|
-
|
|
16
|
-
// NEXTAUTH_SECRET="This is an example"
|
|
17
|
-
// NEXTAUTH_URL
|
|
18
|
-
|
|
19
|
-
// import { SessionProvider } from "next-auth/react"
|
|
20
|
-
// export default function App({
|
|
21
|
-
// Component,
|
|
22
|
-
// pageProps: { session, ...pageProps },
|
|
23
|
-
// }) {
|
|
24
|
-
// return (
|
|
25
|
-
// <SessionProvider session={session}>
|
|
26
|
-
// <Component {...pageProps} />
|
|
27
|
-
// </SessionProvider>
|
|
28
|
-
// )
|
|
29
|
-
// }
|
|
30
|
-
|
|
31
|
-
// const handler = NextAuth({
|
|
32
|
-
// adapter: DrizzleAuthAdapterPG(db),
|
|
33
|
-
// providers: [
|
|
34
|
-
// EmailProvider({
|
|
35
|
-
// server: {
|
|
36
|
-
// host: process.env.SMTP_HOST,
|
|
37
|
-
// port: Number(process.env.SMTP_PORT),
|
|
38
|
-
// auth: {
|
|
39
|
-
// user: process.env.SMTP_USER,
|
|
40
|
-
// pass: process.env.SMTP_PASSWORD,
|
|
41
|
-
// },
|
|
42
|
-
// },
|
|
43
|
-
// from: process.env.EMAIL_FROM,
|
|
44
|
-
// }),
|
|
45
|
-
// GoogleProvider({
|
|
46
|
-
// clientId: process.env.GOOGLE_CLIENT_ID,
|
|
47
|
-
// clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
48
|
-
// }),
|
|
49
|
-
// ],
|
|
50
|
-
// });
|
services/todos.service.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { eq, asc } from 'drizzle-orm';
|
|
2
|
-
import db, { todos } from "@/db";
|
|
3
|
-
|
|
4
|
-
export const getTodos = async () => {
|
|
5
|
-
return await db.select().from(todos).orderBy(asc(todos.id));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
* @param {typeof todos} item
|
|
11
|
-
* @returns
|
|
12
|
-
*/
|
|
13
|
-
export const createTodo = async (item) => {
|
|
14
|
-
return await db.insert(todos).values(item).returning();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const getTodo = async (id) => {
|
|
18
|
-
const results = await db.select().from(todos).where(eq(todos.id, id));
|
|
19
|
-
return results[0]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const updateTodo = async (item) => {
|
|
23
|
-
return await db.update(todos).set(item).where(eq(todos.id, item.id)).returning();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const deleteTodo = async (id) => {
|
|
27
|
-
return await db.delete(todos).where(eq(todos.id, id)).returning();
|
|
28
|
-
}
|
static/favicon.ico
DELETED
|
Binary file
|
static/logo192.png
DELETED
|
Binary file
|
static/logo512.png
DELETED
|
Binary file
|
static/manifest.json
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"short_name": "React App",
|
|
3
|
-
"name": "Create React App Sample",
|
|
4
|
-
"icons": [
|
|
5
|
-
{
|
|
6
|
-
"src": "favicon.ico",
|
|
7
|
-
"sizes": "64x64 32x32 24x24 16x16",
|
|
8
|
-
"type": "image/x-icon"
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"src": "logo192.png",
|
|
12
|
-
"type": "image/png",
|
|
13
|
-
"sizes": "192x192"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"src": "logo512.png",
|
|
17
|
-
"type": "image/png",
|
|
18
|
-
"sizes": "512x512"
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"start_url": ".",
|
|
22
|
-
"display": "standalone",
|
|
23
|
-
"theme_color": "#000000",
|
|
24
|
-
"background_color": "#ffffff"
|
|
25
|
-
}
|
static/robots.txt
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
# https://www.robotstxt.org/robotstxt.html
|
|
2
|
-
User-agent: *
|
|
3
|
-
Disallow:
|