├── .gitignore
├── .dockerignore
├── packages
├── parabola
│ ├── src
│ │ ├── index.ts
│ │ ├── bus.ts
│ │ ├── dispatcher.ts
│ │ ├── renderer.ts
│ │ ├── server.tsx
│ │ └── parabola.js
│ ├── package.json
│ ├── tsconfig.json
│ └── .gitignore
└── example
│ ├── src
│ ├── styles.css
│ ├── pages
│ │ ├── counter.tsx
│ │ ├── poll.tsx
│ │ ├── grid.tsx
│ │ ├── chat.tsx
│ │ ├── views.tsx
│ │ └── main.tsx
│ └── index.tsx
│ ├── tailwind.config.js
│ ├── README.md
│ ├── tsconfig.json
│ ├── package.json
│ └── .gitignore
├── bun.lockb
├── package.json
├── README.md
├── Dockerfile
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .git/
--------------------------------------------------------------------------------
/packages/parabola/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Parabola } from "./server";
2 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webdevcody/parabolajs/HEAD/bun.lockb
--------------------------------------------------------------------------------
/packages/example/src/styles.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "parabolajs",
3 | "private": true,
4 | "workspaces": [
5 | "packages/*"
6 | ],
7 | "scripts": {
8 | "dev": "cd packages/example && bun run dev",
9 | "example": "cd packages/example && bun run start"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/example/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./src/**/*.{html,js,tsx,jsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | daisyui: {
8 | themes: ["night"],
9 | },
10 | plugins: [require("daisyui")],
11 | };
12 |
--------------------------------------------------------------------------------
/packages/example/README.md:
--------------------------------------------------------------------------------
1 | # @parabolajs/example
2 |
3 | To install dependencies:
4 |
5 | ```bash
6 | bun install
7 | ```
8 |
9 | To run:
10 |
11 | ```bash
12 | bun run index.js
13 | ```
14 |
15 | This project was created using `bun init` in bun v1.1.12. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # parabolajs
2 |
3 | this is a work in progress and a prototype
4 |
5 | ```bash
6 | bun install
7 | ```
8 |
9 | To run the example website:
10 |
11 | ```bash
12 | bun run dev
13 | ```
14 |
15 | ## Workflow
16 |
17 | Just run the example website and then modify the framework code as needed inside packages/parabola.
18 |
--------------------------------------------------------------------------------
/packages/parabola/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@parabolajs/parabola",
3 | "main": "src/index",
4 | "type": "module",
5 | "devDependencies": {
6 | "@types/bun": "latest",
7 | "tailwindcss": "^3.4.6"
8 | },
9 | "peerDependencies": {
10 | "typescript": "^5.0.0"
11 | },
12 | "dependencies": {
13 | "hono": "^4.5.0"
14 | },
15 | "scripts": {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext", "DOM"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "allowJs": true,
9 | "jsx": "react-jsx",
10 | "jsxImportSource": "hono/jsx",
11 | // Bundler mode
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "noEmit": true,
16 |
17 | // Best practices
18 | "strict": true,
19 | "skipLibCheck": true,
20 | "noFallthroughCasesInSwitch": true,
21 |
22 | // Some stricter flags (disabled by default)
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "noPropertyAccessFromIndexSignature": false
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/parabola/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext", "DOM"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "allowJs": true,
9 | "jsx": "react-jsx",
10 | "jsxImportSource": "hono/jsx",
11 | // Bundler mode
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "noEmit": true,
16 |
17 | // Best practices
18 | "strict": true,
19 | "skipLibCheck": true,
20 | "noFallthroughCasesInSwitch": true,
21 |
22 | // Some stricter flags (disabled by default)
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "noPropertyAccessFromIndexSignature": false
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG BUN_VERSION=1.1.12
2 | FROM oven/bun:${BUN_VERSION}-slim as base
3 |
4 | WORKDIR /app
5 |
6 | ENV NODE_ENV="production"
7 |
8 | FROM base as build
9 |
10 | RUN apt-get update -qq && \
11 | apt-get install --no-install-recommends -y build-essential pkg-config python-is-python3
12 |
13 | RUN mkdir -p packages/example && \
14 | mkdir -p packages/parabola
15 | COPY --link bun.lockb package.json ./
16 | COPY --link ./packages/example/package.json ./packages/example
17 | COPY --link ./packages/parabola/package.json ./packages/parabola
18 | RUN bun install --ci
19 |
20 | COPY --link . .
21 |
22 | RUN cd packages/example && bun build:tailwind
23 |
24 | FROM base
25 |
26 | COPY --from=build /app /app
27 |
28 | ARG RAILWAY_GIT_COMMIT_SHA
29 | ENV COMMIT_SHA=${RAILWAY_GIT_COMMIT_SHA}
30 |
31 | EXPOSE 3000
32 | CMD [ "bun", "example" ]
--------------------------------------------------------------------------------
/packages/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@parabolajs/example",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "dev": "run-p server tailwind",
7 | "server": "bun --watch src/index.tsx",
8 | "start": "bun src/index.tsx",
9 | "tailwind": "tailwindcss -i src/styles.css -o dist/styles.css --watch",
10 | "build:tailwind": "tailwindcss -i src/styles.css -o dist/styles.css"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "MIT",
15 | "description": "",
16 | "dependencies": {
17 | "@parabolajs/parabola": "workspace:*",
18 | "bad-words": "^3.0.4",
19 | "daisyui": "^4.12.10",
20 | "hono": "^4.5.0",
21 | "npm-run-all": "^4.1.5"
22 | },
23 | "devDependencies": {
24 | "@types/bun": "latest"
25 | },
26 | "peerDependencies": {
27 | "typescript": "^5.0.0"
28 | }
29 | }
--------------------------------------------------------------------------------
/packages/parabola/src/bus.ts:
--------------------------------------------------------------------------------
1 | import type { Renderer } from "./renderer";
2 |
3 | type Invalidator = (key: string) => void;
4 |
5 | export class ControlBus {
6 | private renderer: Renderer;
7 | private actions = new Map<
8 | string,
9 | (invalidate: Invalidator, data: any) => void
10 | >();
11 |
12 | constructor(renderer: Renderer) {
13 | this.renderer = renderer;
14 | }
15 |
16 | on(key: string, cb: (invalidate: Invalidator, data: any) => void) {
17 | if (this.actions.has(key)) {
18 | console.error(`Action with key ${key} already exists`);
19 | return;
20 | }
21 | this.actions.set(key, cb);
22 | }
23 |
24 | invoke(key: string, data: any) {
25 | const action = this.actions.get(key);
26 | if (!action) {
27 | return;
28 | }
29 | const invalidate: Invalidator = (key: string) => {
30 | this.renderer.update(key);
31 | };
32 | action(invalidate, data.data);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/example/src/pages/counter.tsx:
--------------------------------------------------------------------------------
1 | import { Parabola } from "@parabolajs/parabola";
2 |
3 | export function registerCounter(parabola: Parabola) {
4 | let count = 0;
5 |
6 | parabola.template("counter", () => {
7 | return (
8 |
9 |
{count}
10 |
11 |
14 |
15 |
18 |
19 |
20 | );
21 | });
22 |
23 | parabola.action("increment", (invalidate) => {
24 | count++;
25 | invalidate("counter");
26 | });
27 |
28 | parabola.action("decrement", (invalidate) => {
29 | count--;
30 | invalidate("counter");
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/packages/parabola/src/dispatcher.ts:
--------------------------------------------------------------------------------
1 | import type { WSContext } from "hono/ws";
2 |
3 | export class Dispatcher {
4 | private subscriptions = new Map>();
5 |
6 | subscribe(ws: WSContext, key: string) {
7 | if (!this.subscriptions.has(key)) {
8 | this.subscriptions.set(key, new Set());
9 | }
10 | this.subscriptions.get(key).add(ws);
11 | }
12 |
13 | unsubscribe(ws: WSContext, key: string) {
14 | this.subscriptions.get(key).delete(ws);
15 | }
16 |
17 | unsubscribeAll(ws: WSContext) {
18 | this.subscriptions.forEach((sockets) => {
19 | sockets.delete(ws);
20 | });
21 | }
22 |
23 | dispatch(key: string, data: string, ws?: WSContext) {
24 | if (ws) {
25 | ws.send(data);
26 | return;
27 | }
28 |
29 | const sockets = this.subscriptions.get(key);
30 | if (!sockets) {
31 | return;
32 | }
33 | for (const socket of sockets) {
34 | socket.send(data.toString());
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/parabola/src/renderer.ts:
--------------------------------------------------------------------------------
1 | import type { WSContext } from "hono/ws";
2 | import type { Dispatcher } from "./dispatcher";
3 |
4 | export class Renderer {
5 | private dispatcher: Dispatcher;
6 | private templates = new Map string>();
7 | private cache = new Map();
8 |
9 | constructor(dispatcher: Dispatcher) {
10 | this.dispatcher = dispatcher;
11 | }
12 |
13 | register(key: string, cb: () => string) {
14 | if (this.templates.has(key)) {
15 | console.error(`Template with key ${key} already exists`);
16 | return;
17 | }
18 | this.templates.set(key, cb);
19 | }
20 |
21 | getTemplateFromCache(key: string) {
22 | return this.cache.get(key);
23 | }
24 |
25 | update(key: string, ws?: WSContext) {
26 | const template = this.templates.get(key);
27 | if (!template) {
28 | return;
29 | }
30 | const html = template().toString();
31 | this.cache.set(key, html);
32 | this.dispatcher.dispatch(key, JSON.stringify({ key, html }), ws);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Web Dev Cody
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/packages/example/src/pages/poll.tsx:
--------------------------------------------------------------------------------
1 | import { Parabola } from "@parabolajs/parabola";
2 |
3 | const options = [
4 | {
5 | id: "1",
6 | text: "Ice cream",
7 | votes: 0,
8 | },
9 | {
10 | id: "2",
11 | text: "Banana Bread",
12 | votes: 0,
13 | },
14 | {
15 | id: "3",
16 | text: "Cookies",
17 | votes: 0,
18 | },
19 | ];
20 |
21 | export function registerPoll(parabola: Parabola) {
22 | parabola.template("poll", () => {
23 | return (
24 |
25 | {options.map((option) => (
26 |
35 | ))}
36 |
37 | );
38 | });
39 |
40 | parabola.action("vote", (invalidate, data) => {
41 | const voteId = data.optionId;
42 | const option = options.find((option) => option.id === voteId);
43 | if (!option) return;
44 | option.votes++;
45 | invalidate("poll");
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/packages/example/src/pages/grid.tsx:
--------------------------------------------------------------------------------
1 | import { Parabola } from "@parabolajs/parabola";
2 |
3 | const GRID_SIZE = 20;
4 |
5 | const grid: boolean[][] = Array.from({ length: GRID_SIZE }, () =>
6 | Array.from({ length: GRID_SIZE }, () => false)
7 | );
8 |
9 | export function registerGrid(parabola: Parabola) {
10 | parabola.template("grid", () => {
11 | return (
12 |
13 |
Toggle anything on this realtime grid!
14 |
15 | {grid.map((row, rowIndex) => (
16 |
17 | ))}
18 |
19 | );
20 | });
21 |
22 | grid.forEach((row, rowIndex) => {
23 | const cols = row;
24 |
25 | parabola.template(`row:${rowIndex}`, () => {
26 | return (
27 |
28 | {cols.map((isToggled, colIndex) => (
29 |
33 | ))}
34 |
35 | );
36 | });
37 |
38 | parabola.action(`toggle:${rowIndex}`, (invalidate, data) => {
39 | const { col } = data;
40 | grid[rowIndex][col] = !grid[rowIndex][col];
41 | invalidate(`row:${rowIndex}`);
42 | });
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/packages/example/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { Parabola } from "@parabolajs/parabola";
2 | import { serveStatic } from "hono/bun";
3 | import { registerMain } from "./pages/main";
4 | import { registerPoll } from "./pages/poll";
5 | import { registerCounter } from "./pages/counter";
6 | import { registerViews } from "./pages/views";
7 | import { registerChat } from "./pages/chat";
8 | import { registerGrid } from "./pages/grid";
9 |
10 | export const parabola = new Parabola({
11 | styles: ["/styles.css"],
12 | routes: [
13 | {
14 | path: "/",
15 | target: "content",
16 | template: "welcome",
17 | },
18 | {
19 | path: "/poll",
20 | target: "content",
21 | template: "poll",
22 | },
23 | {
24 | path: "/views",
25 | target: "content",
26 | template: "views",
27 | },
28 | {
29 | path: "/counter",
30 | target: "content",
31 | template: "counter",
32 | },
33 | {
34 | path: "/chat",
35 | target: "content",
36 | template: "chat",
37 | },
38 | {
39 | path: "/grid",
40 | target: "content",
41 | template: "grid",
42 | },
43 | ],
44 | });
45 |
46 | parabola
47 | .getApp()
48 | .use("/styles.css", serveStatic({ path: "./dist/styles.css" }));
49 |
50 | registerMain(parabola);
51 | registerPoll(parabola);
52 | registerCounter(parabola);
53 | registerViews(parabola);
54 | registerChat(parabola);
55 | registerGrid(parabola);
56 |
--------------------------------------------------------------------------------
/packages/example/src/pages/chat.tsx:
--------------------------------------------------------------------------------
1 | import { Parabola } from "@parabolajs/parabola";
2 | import Filter from "bad-words";
3 |
4 | const MAX_MESSAGES = 100;
5 | const messages: string[] = [];
6 |
7 | const filter = new Filter();
8 |
9 | export function registerChat(parabola: Parabola) {
10 | parabola.template("chat", () => {
11 | return (
12 |
13 |
Send Messages to Everyone!
14 |
15 |
28 |
29 | );
30 | });
31 |
32 | parabola.template("messageList", () => {
33 | return (
34 |
35 | {messages.map((message) => (
36 |
{message}
37 | ))}
38 |
39 | );
40 | });
41 |
42 | parabola.action("sendMessage", (invalidate, data) => {
43 | const message = filter.clean(data.message);
44 | messages.unshift(message);
45 | messages.splice(MAX_MESSAGES);
46 | invalidate("messageList");
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/packages/example/src/pages/views.tsx:
--------------------------------------------------------------------------------
1 | import { Parabola } from "@parabolajs/parabola";
2 |
3 | let views = 0;
4 |
5 | type User = {
6 | name: string;
7 | };
8 |
9 | const users: User[] = [];
10 |
11 | function addUser(invalidate: any) {
12 | const name = Math.random().toString(36).substring(7);
13 | users.push({
14 | name,
15 | });
16 | setTimeout(() => {
17 | users.splice(
18 | users.findIndex((u) => u.name === name),
19 | 1
20 | );
21 | invalidate("users");
22 | }, 120000);
23 | invalidate("users");
24 | }
25 |
26 | export function registerViews(parabola: Parabola) {
27 | parabola.template("views", () => {
28 | return (
29 | <>
30 |
34 |
35 |
people have loaded this example
36 |
37 |
38 |
39 |
40 | >
41 | );
42 | });
43 |
44 | parabola.template("users", () => {
45 | return (
46 |
47 |
48 | Recent Users (random character, clears after 2 min)
49 |
50 |
51 |
52 | {users.map((user) => (
53 |
54 | {user.name.substring(0, 1)}
55 |
56 | ))}
57 |
58 |
59 | );
60 | });
61 |
62 | parabola.template("views:count", () => {
63 | return {views};
64 | });
65 |
66 | parabola.action("views:increment", (invalidate) => {
67 | views++;
68 | invalidate("views:count");
69 | addUser(invalidate);
70 | });
71 | }
72 |
--------------------------------------------------------------------------------
/packages/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | # Logs
4 |
5 | logs
6 | _.log
7 | npm-debug.log_
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Caches
14 |
15 | .cache
16 |
17 | # Diagnostic reports (https://nodejs.org/api/report.html)
18 |
19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
20 |
21 | # Runtime data
22 |
23 | pids
24 | _.pid
25 | _.seed
26 | *.pid.lock
27 |
28 | # Directory for instrumented libs generated by jscoverage/JSCover
29 |
30 | lib-cov
31 |
32 | # Coverage directory used by tools like istanbul
33 |
34 | coverage
35 | *.lcov
36 |
37 | # nyc test coverage
38 |
39 | .nyc_output
40 |
41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
42 |
43 | .grunt
44 |
45 | # Bower dependency directory (https://bower.io/)
46 |
47 | bower_components
48 |
49 | # node-waf configuration
50 |
51 | .lock-wscript
52 |
53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
54 |
55 | build/Release
56 |
57 | # Dependency directories
58 |
59 | node_modules/
60 | jspm_packages/
61 |
62 | # Snowpack dependency directory (https://snowpack.dev/)
63 |
64 | web_modules/
65 |
66 | # TypeScript cache
67 |
68 | *.tsbuildinfo
69 |
70 | # Optional npm cache directory
71 |
72 | .npm
73 |
74 | # Optional eslint cache
75 |
76 | .eslintcache
77 |
78 | # Optional stylelint cache
79 |
80 | .stylelintcache
81 |
82 | # Microbundle cache
83 |
84 | .rpt2_cache/
85 | .rts2_cache_cjs/
86 | .rts2_cache_es/
87 | .rts2_cache_umd/
88 |
89 | # Optional REPL history
90 |
91 | .node_repl_history
92 |
93 | # Output of 'npm pack'
94 |
95 | *.tgz
96 |
97 | # Yarn Integrity file
98 |
99 | .yarn-integrity
100 |
101 | # dotenv environment variable files
102 |
103 | .env
104 | .env.development.local
105 | .env.test.local
106 | .env.production.local
107 | .env.local
108 |
109 | # parcel-bundler cache (https://parceljs.org/)
110 |
111 | .parcel-cache
112 |
113 | # Next.js build output
114 |
115 | .next
116 | out
117 |
118 | # Nuxt.js build / generate output
119 |
120 | .nuxt
121 | dist
122 |
123 | # Gatsby files
124 |
125 | # Comment in the public line in if your project uses Gatsby and not Next.js
126 |
127 | # https://nextjs.org/blog/next-9-1#public-directory-support
128 |
129 | # public
130 |
131 | # vuepress build output
132 |
133 | .vuepress/dist
134 |
135 | # vuepress v2.x temp and cache directory
136 |
137 | .temp
138 |
139 | # Docusaurus cache and generated files
140 |
141 | .docusaurus
142 |
143 | # Serverless directories
144 |
145 | .serverless/
146 |
147 | # FuseBox cache
148 |
149 | .fusebox/
150 |
151 | # DynamoDB Local files
152 |
153 | .dynamodb/
154 |
155 | # TernJS port file
156 |
157 | .tern-port
158 |
159 | # Stores VSCode versions used for testing VSCode extensions
160 |
161 | .vscode-test
162 |
163 | # yarn v2
164 |
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.*
170 |
171 | # IntelliJ based IDEs
172 | .idea
173 |
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 |
--------------------------------------------------------------------------------
/packages/parabola/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | # Logs
4 |
5 | logs
6 | _.log
7 | npm-debug.log_
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Caches
14 |
15 | .cache
16 |
17 | # Diagnostic reports (https://nodejs.org/api/report.html)
18 |
19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
20 |
21 | # Runtime data
22 |
23 | pids
24 | _.pid
25 | _.seed
26 | *.pid.lock
27 |
28 | # Directory for instrumented libs generated by jscoverage/JSCover
29 |
30 | lib-cov
31 |
32 | # Coverage directory used by tools like istanbul
33 |
34 | coverage
35 | *.lcov
36 |
37 | # nyc test coverage
38 |
39 | .nyc_output
40 |
41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
42 |
43 | .grunt
44 |
45 | # Bower dependency directory (https://bower.io/)
46 |
47 | bower_components
48 |
49 | # node-waf configuration
50 |
51 | .lock-wscript
52 |
53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
54 |
55 | build/Release
56 |
57 | # Dependency directories
58 |
59 | node_modules/
60 | jspm_packages/
61 |
62 | # Snowpack dependency directory (https://snowpack.dev/)
63 |
64 | web_modules/
65 |
66 | # TypeScript cache
67 |
68 | *.tsbuildinfo
69 |
70 | # Optional npm cache directory
71 |
72 | .npm
73 |
74 | # Optional eslint cache
75 |
76 | .eslintcache
77 |
78 | # Optional stylelint cache
79 |
80 | .stylelintcache
81 |
82 | # Microbundle cache
83 |
84 | .rpt2_cache/
85 | .rts2_cache_cjs/
86 | .rts2_cache_es/
87 | .rts2_cache_umd/
88 |
89 | # Optional REPL history
90 |
91 | .node_repl_history
92 |
93 | # Output of 'npm pack'
94 |
95 | *.tgz
96 |
97 | # Yarn Integrity file
98 |
99 | .yarn-integrity
100 |
101 | # dotenv environment variable files
102 |
103 | .env
104 | .env.development.local
105 | .env.test.local
106 | .env.production.local
107 | .env.local
108 |
109 | # parcel-bundler cache (https://parceljs.org/)
110 |
111 | .parcel-cache
112 |
113 | # Next.js build output
114 |
115 | .next
116 | out
117 |
118 | # Nuxt.js build / generate output
119 |
120 | .nuxt
121 | dist
122 |
123 | # Gatsby files
124 |
125 | # Comment in the public line in if your project uses Gatsby and not Next.js
126 |
127 | # https://nextjs.org/blog/next-9-1#public-directory-support
128 |
129 | # public
130 |
131 | # vuepress build output
132 |
133 | .vuepress/dist
134 |
135 | # vuepress v2.x temp and cache directory
136 |
137 | .temp
138 |
139 | # Docusaurus cache and generated files
140 |
141 | .docusaurus
142 |
143 | # Serverless directories
144 |
145 | .serverless/
146 |
147 | # FuseBox cache
148 |
149 | .fusebox/
150 |
151 | # DynamoDB Local files
152 |
153 | .dynamodb/
154 |
155 | # TernJS port file
156 |
157 | .tern-port
158 |
159 | # Stores VSCode versions used for testing VSCode extensions
160 |
161 | .vscode-test
162 |
163 | # yarn v2
164 |
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.*
170 |
171 | # IntelliJ based IDEs
172 | .idea
173 |
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 |
--------------------------------------------------------------------------------
/packages/example/src/pages/main.tsx:
--------------------------------------------------------------------------------
1 | import type { Parabola } from "@parabolajs/parabola";
2 |
3 | function Header() {
4 | return (
5 |
28 | );
29 | }
30 |
31 | function Footer() {
32 | return (
33 |
36 | );
37 | }
38 |
39 | export function registerMain(parabola: Parabola) {
40 | parabola.template("main", () => {
41 | return (
42 | <>
43 |
44 |
45 |
52 |
53 |
54 | >
55 | );
56 | });
57 |
58 | parabola.template("welcome", () => {
59 | return (
60 |
61 |
ParabolaJs
62 |
63 |
64 | Everything is realtime and shared using Paraboljs, load up multiple
65 | tabs and try it out. Here are some examples below
66 |
67 |
68 |
114 |
115 | );
116 | });
117 | }
118 |
--------------------------------------------------------------------------------
/packages/parabola/src/server.tsx:
--------------------------------------------------------------------------------
1 | import { Hono } from "hono";
2 | import { createBunWebSocket } from "hono/bun";
3 |
4 | const { upgradeWebSocket, websocket } = createBunWebSocket();
5 |
6 | import { ControlBus } from "./bus";
7 | import { Dispatcher } from "./dispatcher";
8 | import { Renderer } from "./renderer";
9 | import fs from "fs/promises";
10 | import path from "path";
11 |
12 | function Main({ styles, routes }: { styles?: string[]; routes?: any[] }) {
13 | return (
14 |
15 |
16 |
17 | Parabola
18 | {styles?.map((style) => (
19 |
20 | ))}
21 |
22 |
23 | {/* */}
24 |
25 |
26 | loading...
27 |
28 |
29 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
43 | export class Parabola {
44 | private dispatcher: Dispatcher;
45 | private renderer: Renderer;
46 | private controlBus: ControlBus;
47 | private app: Hono;
48 |
49 | getApp() {
50 | return this.app;
51 | }
52 |
53 | constructor(opts?: { styles?: string[]; routes?: any[] }) {
54 | this.dispatcher = new Dispatcher();
55 | this.renderer = new Renderer(this.dispatcher);
56 | this.controlBus = new ControlBus(this.renderer);
57 |
58 | const app = new Hono();
59 | this.app = app;
60 |
61 | app.get("/static/parabola.js", async (c) => {
62 | const filePath = path.join(__dirname, "./parabola.js");
63 | const js = await fs.readFile(filePath, {
64 | encoding: "utf-8",
65 | });
66 | return c.html(js);
67 | });
68 |
69 | app.get("/", (c) => {
70 | return c.html();
71 | });
72 |
73 | app.get(
74 | "/ws",
75 | upgradeWebSocket((c) => {
76 | return {
77 | onOpen(_event, ws) {},
78 | onMessage: (evt, ws) => {
79 | const { type, payload } = JSON.parse(evt.data);
80 | if (type === "template") {
81 | const key = payload;
82 | this.dispatcher.subscribe(ws, key);
83 | const view = this.renderer.getTemplateFromCache(key);
84 | if (!view) {
85 | this.renderer.update(key, ws);
86 | } else {
87 | this.dispatcher.dispatch(
88 | key,
89 | JSON.stringify({ key, html: view }),
90 | ws
91 | );
92 | }
93 | }
94 | if (type === "action") {
95 | this.controlBus.invoke(payload.key, payload);
96 | }
97 | },
98 | onClose: (evt, ws) => {
99 | this.dispatcher.unsubscribeAll(ws);
100 | },
101 | };
102 | })
103 | );
104 |
105 | app.notFound((c) => {
106 | return c.html();
107 | });
108 |
109 | Bun.serve({
110 | fetch: app.fetch,
111 | websocket,
112 | });
113 | }
114 |
115 | invalidate(key: string) {
116 | this.renderer.update(key);
117 | }
118 |
119 | // I'm not sure why this can't be JSXNode... it causes consumers to get type errors
120 | template(key: string, cb: () => JSX.Element) {
121 | this.renderer.register(key, cb);
122 | }
123 |
124 | action(
125 | key: string,
126 | cb: (invalidate: (key: string) => void, data: any) => void
127 | ) {
128 | this.controlBus.on(key, cb);
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/packages/parabola/src/parabola.js:
--------------------------------------------------------------------------------
1 | function connect() {
2 | const ws = new WebSocket("/ws");
3 | const subscriptions = new Map();
4 |
5 | let isFirstPageLoad = true;
6 |
7 | function register(parent = document) {
8 | registerTemplates(parent);
9 | registerActions(parent);
10 | registerHref(parent);
11 | registerLoad(parent);
12 | }
13 |
14 | function clearSubscriptions() {
15 | subscriptions.clear();
16 | }
17 |
18 | window.onpopstate = (event) => {
19 | if (event.state) {
20 | const targetEl = document.getElementById(event.state.target);
21 | targetEl.innerHTML = ``;
22 | clearSubscriptions();
23 | register(targetEl);
24 | }
25 | };
26 |
27 | function registerLoad(parent = document) {
28 | const elements = parent.querySelectorAll("[p-load]");
29 | elements.forEach((element) => {
30 | const key = element.getAttribute("p-load");
31 | ws.send(JSON.stringify({ type: "action", payload: { key, data: null } }));
32 | });
33 | }
34 |
35 | function registerHref(parent = document) {
36 | const elements = parent.querySelectorAll("[p-href]");
37 | elements.forEach((element) => {
38 | const href = element.getAttribute("p-href");
39 | const target = element.getAttribute("p-target");
40 | const swap = element.getAttribute("p-swap");
41 | const targetEl = document.getElementById(target);
42 |
43 | function onClick(event) {
44 | event.preventDefault();
45 | history.pushState(
46 | {
47 | template: swap,
48 | target,
49 | },
50 | "",
51 | href
52 | );
53 | targetEl.innerHTML = ``;
54 | clearSubscriptions();
55 | register(targetEl);
56 | }
57 |
58 | element.removeEventListener("click", onClick);
59 | element.addEventListener("click", onClick);
60 | });
61 | }
62 |
63 | function registerTemplates(parent = document) {
64 | const path = window.location.pathname;
65 |
66 | const elements = parent.querySelectorAll("[p-template]");
67 | elements.forEach((element) => {
68 | let key = element.getAttribute("p-template");
69 | const id = element.getAttribute("id");
70 |
71 | for (const route of window.parabolaRoutes) {
72 | if (route.path === path && id === route.target) {
73 | key = route.template;
74 | element.setAttribute("p-template", key);
75 | history.replaceState(
76 | {
77 | template: key,
78 | target: route.target,
79 | },
80 | ""
81 | );
82 | }
83 | }
84 | if (!subscriptions.has(key)) {
85 | subscriptions.set(key, new Set());
86 | }
87 | subscriptions.get(key).add(element);
88 | ws.send(JSON.stringify({ type: "template", payload: key }));
89 | });
90 | }
91 |
92 | function registerActions(parent = document) {
93 | const forms = parent.querySelectorAll("[p-action]");
94 | forms.forEach((form) => {
95 | function onSubmit(event) {
96 | event.preventDefault();
97 | const key = form.getAttribute("p-action");
98 | const formData = new FormData(form);
99 | const data = Object.fromEntries(formData);
100 | form.reset();
101 | ws.send(JSON.stringify({ type: "action", payload: { key, data } }));
102 | }
103 | form.removeEventListener("submit", onSubmit);
104 | form.addEventListener("submit", onSubmit);
105 | });
106 | }
107 |
108 | ws.onmessage = (event) => {
109 | const data = JSON.parse(event.data);
110 | const subscription = subscriptions.get(data.key);
111 | if (!subscription) {
112 | return;
113 | }
114 | const template = document.createElement("template");
115 | template.innerHTML = data.html;
116 |
117 | for (const element of subscription) {
118 | element.innerHTML = "";
119 | element.appendChild(template.content.cloneNode(true));
120 | register(element);
121 | }
122 | };
123 |
124 | ws.onclose = () => {
125 | setTimeout(() => {
126 | connect();
127 | }, 1000);
128 | };
129 |
130 | ws.onopen = () => {
131 | register();
132 | isFirstPageLoad = false;
133 | };
134 | }
135 |
136 | connect();
137 |
--------------------------------------------------------------------------------