~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


5636e804 Peter John

2 years ago
fix merge
.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
- // TODO:
288
+ // TODO:
285
- // if (bot || isCrawler) {
286
- // await stream.allReady
287
- // add helmetContext to head
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 next app router) with streaming SSR + CSR render pages.
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
- Because its really hard to have a streaming SSR + CSR setup in nextjs currently.
15
+ * Its really hard to have a streaming SSR + CSR setup in nextjs currently.
15
- The only other framework is rakkasjs but it doesn't maitaing as smooth transition from nextjs.
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
- 3. Add Env variables `PUBLIC_` for client
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: