├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bun.lock ├── docs ├── Hyper.svg ├── h(⚡️).svg ├── thomas.jpg └── versions.md ├── package.json ├── packages ├── blink │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── assets │ │ ├── img │ │ │ ├── 1200x630.jpg │ │ │ ├── favicon-96x96.png │ │ │ └── favicon.ico │ │ ├── normalise.css │ │ └── style.css │ ├── example.config.json │ ├── package.json │ ├── src │ │ ├── config.ts │ │ ├── index.ts │ │ ├── middleware │ │ │ └── auth.ts │ │ ├── pages │ │ │ ├── auth.ts │ │ │ ├── dashboard.ts │ │ │ └── links.ts │ │ ├── setup.ts │ │ ├── store.ts │ │ ├── types.ts │ │ └── utils.ts │ └── tsconfig.json ├── h2h │ ├── LICENSE │ ├── h2h.js │ └── index.html ├── hyper │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── scripts │ │ ├── prepack.ts │ │ └── publish.sh │ ├── src │ │ ├── attributes.ts │ │ ├── browser.test.ts │ │ ├── context.internal.ts │ │ ├── context.test.ts │ │ ├── context.ts │ │ ├── domutils.ts │ │ ├── element.ts │ │ ├── elements.ts │ │ ├── guessEnv.ts │ │ ├── index.html │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── aria.ts │ │ │ ├── attributes.ts │ │ │ ├── dom.extra.ts │ │ │ ├── dom.ts │ │ │ ├── emptyElements.ts │ │ │ ├── global-attributes.ts │ │ │ └── tags.ts │ │ ├── list.test.ts │ │ ├── list.ts │ │ ├── node.test.ts │ │ ├── node.ts │ │ ├── parse.ts │ │ ├── render │ │ │ ├── dom.ts │ │ │ └── html.ts │ │ ├── router.ts │ │ ├── state.test.ts │ │ ├── state.ts │ │ └── util.ts │ └── tsconfig.json ├── mark │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── reference.hm │ ├── src │ │ ├── Context.ts │ │ ├── block.ts │ │ ├── common.ts │ │ ├── hypermark.ts │ │ ├── index.ts │ │ ├── inline.ts │ │ ├── parser.test.ts │ │ ├── types.ts │ │ ├── value.test.ts │ │ └── value.ts │ └── tsconfig.json ├── scripts │ ├── domlib.ts │ ├── fetchARIA.ts │ ├── fetchAttributes.ts │ ├── fetchGlobalAttributes.ts │ ├── fetchTags.ts │ ├── gen.ts │ ├── package.json │ └── util │ │ ├── getSpecialType.ts │ │ ├── html2md.ts │ │ └── hypertyper.ts ├── serve │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── comments.txt │ │ ├── core.ts │ │ ├── eventsource.ts │ │ ├── index.ts │ │ ├── methods.ts │ │ ├── mod.ts │ │ ├── passthrough.ts │ │ ├── serve.test.ts │ │ ├── spec.ts │ │ ├── utils.ts │ │ └── ws.ts │ └── tsconfig.json ├── todo │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-96x96.png │ │ │ ├── favicon.ico │ │ │ ├── site.webmanifest │ │ │ ├── web-app-manifest-192x192.png │ │ │ └── web-app-manifest-512x512.png │ ├── src │ │ ├── App.ts │ │ ├── _ │ │ │ ├── diff.tsx │ │ │ ├── hyper.tsx │ │ │ └── index.tsx │ │ └── styles.css │ ├── tsconfig.json │ └── vite.config.ts ├── url │ ├── .gitignore │ ├── README.md │ ├── blog │ │ ├── blog.md │ │ └── express.jpg │ ├── package.json │ ├── src │ │ ├── HyperURL.ts │ │ ├── index.ts │ │ ├── parse.test.ts │ │ └── parse.ts │ └── tsconfig.json └── web │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ └── assets │ │ ├── fonts │ │ ├── GeistMono[wght].woff2 │ │ ├── Geist[wght].woff2 │ │ └── fonts.css │ │ ├── prism │ │ ├── prism.css │ │ └── prism.js │ │ └── style.css │ ├── src │ ├── content │ │ └── docs.md │ ├── index.ts │ └── pages │ │ ├── home.ts │ │ └── layout.ts │ └── tsconfig.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | browser-test/test.browser.js 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "useTabs": true, 4 | "semi": true, 5 | "singleQuote": false, 6 | "quoteProps": "consistent", 7 | "jsxSingleQuote": false, 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": true, 10 | "arrowParens": "avoid", 11 | "printWidth": 100 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "deno.enable": false 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Feathers Studio 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
value
other value
hello 123
"); 46 | }); 47 | 48 | it("should handle objects as context values", () => { 49 | type MyObject = { message: string; count: number }; 50 | const objectContext = new Contextcustom 42
"); 58 | }); 59 | 60 | it("should allow null as a provided context value", () => { 61 | // Default value is non-null to distinguish it from an explicit null 62 | const nullableContext = new Contextwas null
"); 70 | }); 71 | 72 | it("should allow undefined as a provided context value", () => { 73 | const undefinedContext = new Contextwas undefined
"); 80 | }); 81 | 82 | it("should handle functions as context values", () => { 83 | const funcContext = new Context<() => string>(() => () => "default func"); 84 | 85 | const tree = funcContext.provider( 86 | () => "dynamic value from func", 87 | funcContext.with(fn => p(fn())), 88 | ); 89 | expect(renderHTML(tree)).toBe("dynamic value from func
"); 90 | }); 91 | 92 | it("multiple sibling consumers should receive the same context value", () => { 93 | const sharedContext = new Contextshared
sharedButton A: dark
Button B: contrast
Button C: dark
A: Value A
"); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /packages/hyper/src/context.ts: -------------------------------------------------------------------------------- 1 | import { HyperNodeish } from "./node.ts"; 2 | import { randId } from "./util.ts"; 3 | import * as ContextInternal from "./context.internal.ts"; 4 | 5 | export class Context<test />
`); 84 | }); 85 | 86 | test("renderHTML with trusted HTML", () => { 87 | expect(renderHTML(p(trust("(?.+?)<\/code>/g, (_, _1, _2, _3, { contents }) => contents)
17 | .replace(/(?.+?)<\/strong>/g, (_, _1, _2, _3, { contents }) => `**${contents}**`)
18 | .replace(
19 | /(?.+?)<\/a>/g,
20 | (_, _1, _2, _3, _4, { contents, href }) => `[${contents}](${new URL(href, baseURL).toString()})`,
21 | ),
22 | );
23 |
--------------------------------------------------------------------------------
/packages/serve/.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/serve/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Feathers Studio
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.
22 |
--------------------------------------------------------------------------------
/packages/serve/README.md:
--------------------------------------------------------------------------------
1 | # hyperserve
2 |
3 | Part of the [hyperactive](https://github.com/feathers-studio/hyperactive) project. A lean server library that's functional by design and integrates seamlessly with hyperactive itself.
4 |
5 | ## Getting started
6 |
7 | ```TypeScript
8 | import { get, router, serve } from "https://deno.land/x/hyperactive/serve.ts";
9 |
10 | const server = serve(
11 | { port: 3000 },
12 | router(
13 | get("/", (ctx, next) => next(), (ctx) => ctx.respond("Hello world")),
14 | get("/foo", (ctx) => ctx.respond("Foo")),
15 | get("/bar", (ctx) => ctx.respond("Bar")),
16 | ),
17 | );
18 |
19 | console.log("Listening on port", 3000);
20 | server.start();
21 | ```
22 |
23 | ## Using websockets
24 |
25 | `hyperserve` supports websockets straight in your application! It's as simple as calling the `ws` util.
26 |
27 | ```TypeScript
28 | serve(
29 | { port: 3000 },
30 | router(
31 | get("/", (ctx, next) => next(), (ctx) => ctx.respond("Hello world")),
32 | ws("/foo", async (socket) => {
33 | socket.addEventListener("message", (e) => console.log(e.data));
34 | }),
35 | ),
36 | );
37 | ```
38 |
39 | ## Using server-sent events
40 |
41 | [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) is an alternative to websockets when you only need to publish data one-way from the server.
42 |
43 | ```TypeScript
44 | serve(
45 | { port: 3000 },
46 | router(
47 | get("/", (ctx, next) => next(), (ctx) => ctx.respond("Hello world")),
48 | eventsource("/notifs", async (ctx) => {
49 | setInterval(() => {
50 | // Sends a notification every second
51 | ctx.event({ event: "notification", data: "You have a message" });
52 | }, 1000);
53 | }),
54 | ),
55 | );
56 | ```
57 |
--------------------------------------------------------------------------------
/packages/serve/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hyperactive/serve",
3 | "module": "src/index.ts",
4 | "type": "module",
5 | "devDependencies": {
6 | "@types/bun": "latest"
7 | },
8 | "peerDependencies": {
9 | "typescript": "^5.0.0"
10 | },
11 | "exports": {
12 | ".": "./src/index.ts",
13 | "./utils": "./src/utils.ts"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/serve/src/comments.txt:
--------------------------------------------------------------------------------
1 | Request -> Response is very nice for testing
2 |
3 | Since Request#ip doesn't exist, so Bun had to invent server.requestIP(request)
4 |
5 | For middleware-like things, Node style ServerResponse objects are more useful than standard Response; for example, a middleware that checks if user is logged in and extends their session expiry would need to Set-Cookie unrelated to the actual request handler
6 |
7 | Request -> Response pattern has yet another big issue; you cannot do something after response is sent (like logging request time)
--------------------------------------------------------------------------------
/packages/serve/src/core.ts:
--------------------------------------------------------------------------------
1 | import { type HyperNode, renderHTML } from "../hyper/mod.ts";
2 |
3 | export type Next = () => Promise;
4 |
5 | export type Middleware = (ctx: Context, next: Next) => Promise;
6 |
7 | export type Context = {
8 | request: Request;
9 | responded: boolean;
10 | respond: (body?: Response | BodyInit | null, init?: ResponseInit) => Promise;
11 | html: (body: string, init?: ResponseInit) => Promise;
12 | render: (body: HyperNode, init?: ResponseInit) => Promise;
13 | state: Partial;
14 | };
15 |
16 | export function o(f: Middleware, g: Middleware): Middleware {
17 | return (ctx: Context, next: Next) => g(ctx, () => f(ctx, next));
18 | }
19 |
20 | export function router(...fs: Middleware[]): Middleware {
21 | if (fs.length === 0) throw new TypeError("router requires at least one Middleware");
22 | return fs.reduceRight(o);
23 | }
24 |
25 | const h404: Middleware = ctx => {
26 | return ctx.respond(`Cannot ${ctx.request.method} ${new URL(ctx.request.url).pathname}`, { status: 404 });
27 | };
28 |
29 | function Context(e: Deno.RequestEvent): Context {
30 | let responded = false;
31 |
32 | const self: Context = {
33 | request: e.request,
34 | get responded() {
35 | return responded;
36 | },
37 | respond(body, init) {
38 | if (responded) throw new Error("Can't call respond() twice");
39 | responded = true;
40 | return e.respondWith(body instanceof Response ? body : new Response(body, init));
41 | },
42 | html(body, init) {
43 | const headers = new Headers(init?.headers);
44 | headers.set("Content-Type", "text/html; charset=UTF-8");
45 | return self.respond(body, {
46 | headers,
47 | status: init?.status,
48 | statusText: init?.statusText,
49 | });
50 | },
51 | render(body, init) {
52 | return self.html("" + renderHTML(body), init);
53 | },
54 | state: {},
55 | };
56 |
57 | return self;
58 | }
59 |
60 | export const noop = async (): Promise => void 0;
61 |
62 | export function serve(opts: Deno.ListenOptions, handler: Middleware) {
63 | async function handleHttp(conn: Deno.Conn) {
64 | for await (const e of Deno.serveHttp(conn)) {
65 | const ctx = Context(e);
66 | handler(ctx, () => h404(ctx, noop));
67 | }
68 | }
69 |
70 | const server = Deno.listen(opts);
71 |
72 | async function start() {
73 | for await (const conn of server) handleHttp(conn);
74 | }
75 |
76 | return {
77 | start,
78 | close() {
79 | server.close();
80 | },
81 | };
82 | }
83 |
84 | export function filter(
85 | predicate: (ctx: Context) => boolean,
86 | middleware: Middleware,
87 | ): Middleware {
88 | return (ctx, next) => (predicate(ctx) ? middleware(ctx, next) : next());
89 | }
90 |
--------------------------------------------------------------------------------
/packages/serve/src/eventsource.ts:
--------------------------------------------------------------------------------
1 | import { readableStreamFromReader, writeAll } from "https://deno.land/std@0.125.0/streams/conversion.ts";
2 |
3 | import { type Context, filter, type Middleware } from "./core.ts";
4 | import { PassThrough } from "./passthrough.ts";
5 |
6 | export type Event = {
7 | event?: string;
8 | data: string;
9 | id?: string;
10 | retry?: number;
11 | };
12 |
13 | export type EventSourceContext = {
14 | request: Request;
15 | comment: (content: string) => Promise;
16 | event: (e: Event) => Promise;
17 | ended: boolean;
18 | startResponse: (headers?: HeadersInit) => Promise;
19 | state: Partial;
20 | };
21 |
22 | type EventSourceHandler = (ctx: EventSourceContext) => Promise;
23 |
24 | /**
25 | * Converts `{ data: "Hello\nWorld", id: "1" }` to
26 | *
27 | * ```
28 | * data: Hello
29 | * data: World
30 | * id: 1
31 | * ```
32 | */
33 | function eventToString(o: Record) {
34 | return Object.entries(o)
35 | .map(([k, v]) =>
36 | String(v)
37 | .split("\n")
38 | .map(line => `${k}: ${line}`)
39 | .join("\n"),
40 | )
41 | .join("\n");
42 | }
43 |
44 | async function write(buf: PassThrough, content: string) {
45 | if (!content) return;
46 | return void (await writeAll(buf, new TextEncoder().encode(content + "\n\n")));
47 | }
48 |
49 | function EventSourceContext(ctx: Context): EventSourceContext {
50 | const passthrough = new PassThrough();
51 | const stream = readableStreamFromReader(passthrough);
52 |
53 | const self: EventSourceContext = {
54 | request: ctx.request,
55 | comment: content => write(passthrough, ": " + content),
56 | event: event => write(passthrough, eventToString(event)),
57 | ended: false,
58 | startResponse: headersInit => {
59 | const headers = new Headers(headersInit);
60 | headers.set("Cache-Control", "no-store");
61 | headers.set("Content-Type", "text/event-stream");
62 | return ctx
63 | .respond(stream, { headers })
64 | .then(() => {
65 | self.ended = true;
66 | })
67 | .catch(e => {
68 | self.ended = true;
69 | return Promise.reject(e);
70 | });
71 | },
72 | state: ctx.state,
73 | };
74 |
75 | return self as EventSourceContext;
76 | }
77 |
78 | export function eventsource(pattern: string, handler: EventSourceHandler): Middleware {
79 | const urlPattern = new URLPattern({ pathname: pattern });
80 | const pred = (ctx: Context) => urlPattern.test(ctx.request.url);
81 | return filter(pred, async ctx => {
82 | return handler(EventSourceContext(ctx));
83 | });
84 | }
85 |
--------------------------------------------------------------------------------
/packages/serve/src/index.ts:
--------------------------------------------------------------------------------
1 | console.log("Hello via Bun!");
--------------------------------------------------------------------------------
/packages/serve/src/methods.ts:
--------------------------------------------------------------------------------
1 | import { type Context, filter, type Middleware, router } from "./core.ts";
2 |
3 | type Methods = "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" | "PATCH";
4 |
5 | export function method(m: Methods) {
6 | return (pattern: string, ...middleware: Middleware[]): Middleware => {
7 | const urlPattern = new URLPattern({ pathname: pattern });
8 | const pred = (ctx: Context) => ctx.request.method === m && urlPattern.test(ctx.request.url);
9 | return filter(pred, router(...middleware));
10 | };
11 | }
12 |
13 | export const options = method("OPTIONS");
14 | export const get = method("GET");
15 | export const post = method("POST");
16 | export const put = method("PUT");
17 | export const patch = method("PATCH");
18 | export const del = method("DELETE");
19 |
20 | export const use = router;
21 |
--------------------------------------------------------------------------------
/packages/serve/src/mod.ts:
--------------------------------------------------------------------------------
1 | export * from "./core.ts";
2 | export * from "./methods.ts";
3 | export * from "./ws.ts";
4 | export * from "./eventsource.ts";
5 |
--------------------------------------------------------------------------------
/packages/serve/src/serve.test.ts:
--------------------------------------------------------------------------------
1 | import { readableStreamFromReader } from "https://deno.land/std@0.125.0/streams/conversion.ts";
2 | import { type Context, eventsource, get, type Middleware, router, serve, ws } from "./mod.ts";
3 | import { PassThrough } from "./passthrough.ts";
4 |
5 | type User = { name: string };
6 | const State = new WeakMap();
7 |
8 | const sleep = (t: number) => new Promise(r => setTimeout(r, t));
9 |
10 | type Cookies = { cookies: string[] };
11 | const cookies: Middleware = (ctx, next) => next();
12 |
13 | const server = serve(
14 | { port: 4000 },
15 | router(
16 | cookies,
17 | router(
18 | get("/", ctx => {
19 | const url = new URL(ctx.request.url);
20 | new URL("", url);
21 | return ctx.respond(
22 | JSON.stringify({
23 | hash: url.hash,
24 | host: url.host,
25 | hostname: url.hostname,
26 | href: url.href,
27 | origin: url.origin,
28 | password: url.password,
29 | pathname: url.pathname,
30 | port: url.port,
31 | protocol: url.protocol,
32 | search: url.search,
33 | searchParams: [...url.searchParams.entries()],
34 | }),
35 | );
36 | }),
37 | // get("/", (ctx, next) => {
38 | // const passthrough = new PassThrough();
39 | // let cancel = false;
40 | // setInterval(() => {
41 | // if (cancel) return;
42 | // passthrough.write(new TextEncoder().encode("Hello\r\n"));
43 | // }, 1000);
44 | // return ctx.respond(readableStreamFromReader(passthrough), { headers: { "Transfer-Encoding": "chunked" } })
45 | // .catch(() => {
46 | // cancel = true;
47 | // console.log("Caught");
48 | // });
49 | // }),
50 | // get("/favicon.ico", (ctx) => ctx.respond(new ArrayBuffer(0))),
51 | // ws("/socket", async (socket) => {
52 | // socket.addEventListener("message", (e) => console.log(e.data));
53 | // // socket.addEventListener("");
54 | // }),
55 | // get("/", (ctx) => {
56 | // return ctx.respond(ctx.state.user?.name);
57 | // }),
58 | // eventsource("/notifs", async (ctx) => {
59 | // try {
60 | // const resp = ctx.startResponse();
61 |
62 | // console.log("connected");
63 | // await ctx.comment("comment");
64 | // await sleep(1000);
65 | // await ctx.event({ data: "helloxxx", event: "hello" });
66 |
67 | // await resp;
68 | // } catch (e) {
69 | // console.log(e);
70 | // }
71 | // }),
72 | ),
73 | ),
74 | );
75 |
76 | server.start();
77 | console.log("Listening on port", 4000);
78 |
--------------------------------------------------------------------------------
/packages/serve/src/spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | post(
4 | "/:userId/posts?search&advanced&count",
5 |
6 | // params only parses
7 |
8 | /*
9 | userId always exists as string,
10 | but params() parses it to number
11 | */
12 | params({
13 | userId: number,
14 | groupId: number,
15 | //^^^^^ TS ERROR
16 | }),
17 |
18 | // query validates and parses
19 |
20 | /*
21 | search always exists as ?: string
22 | query() ensures it must exist as string
23 | it also parses the existence of advanced to boolean
24 | and parses count to number
25 | */
26 | query({
27 | search: string,
28 | advanced: boolean,
29 | count: number,
30 | }),
31 |
32 | // body only validates
33 |
34 | body({
35 | searchToken: string,
36 | }),
37 |
38 | handler,
39 | );
40 |
--------------------------------------------------------------------------------
/packages/serve/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { stat } from "node:fs/promises";
2 |
3 | export const days = (n: number) => n * 60 * 60 * 24;
4 | export const hours = (n: number) => n * 60 * 60;
5 | export const minutes = (n: number) => n * 60;
6 | export const seconds = (n: number) => n;
7 |
8 | export async function generateETagFromFile(filePath: string): Promise {
9 | const stats = await stat(filePath);
10 | const mtime = stats.mtime.getTime().toString();
11 | const size = stats.size.toString();
12 | const rawETag = `${mtime}-${size}`;
13 |
14 | const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawETag));
15 | const hashArray = new Uint8Array(hashBuffer);
16 | let hashHex = "";
17 | for (let i = 0; i < hashArray.length; i++) {
18 | hashHex += hashArray[i].toString(16).padStart(2, "0");
19 | }
20 | return `"${hashHex}"`;
21 | }
22 |
23 | export const redirect = (url: string, status: 301 | 302 = 302, headers: Record = {}) =>
24 | new Response(null, { status, headers: { Location: url, ...headers } });
25 |
26 | export const json = (body: any, status = 200, headers: Record = {}) =>
27 | new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...headers } });
28 |
29 | export const html = (content: string, status = 200, headers: Record = {}) =>
30 | new Response("" + content, { status, headers: { "Content-Type": "text/html", ...headers } });
31 |
--------------------------------------------------------------------------------
/packages/serve/src/ws.ts:
--------------------------------------------------------------------------------
1 | import { type Context, filter, type Middleware } from "./core.ts";
2 |
3 | export type WebSocketHandler = (socket: WebSocket) => Promise;
4 |
5 | export function ws(pattern: string, handler: WebSocketHandler): Middleware {
6 | const urlPattern = new URLPattern({ pathname: pattern });
7 | const pred = (ctx: Context) => urlPattern.test(ctx.request.url);
8 | return filter(pred, async ctx => {
9 | const { response, socket } = Deno.upgradeWebSocket(ctx.request);
10 | await ctx.respond(response);
11 | return handler(socket);
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/packages/serve/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext", "DOM"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "jsx": "react-jsx",
9 | "allowJs": true,
10 |
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/todo/.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/todo/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Feathers Studio
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.
22 |
--------------------------------------------------------------------------------
/packages/todo/README.md:
--------------------------------------------------------------------------------
1 | # @feathers-studio/todo
2 |
3 | To install dependencies:
4 |
5 | ```bash
6 | bun install
7 | ```
8 |
9 | To run:
10 |
11 | ```bash
12 | bun run src/index.ts
13 | ```
14 |
15 | This project was created using `bun init` in bun v1.1.42. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
16 |
--------------------------------------------------------------------------------
/packages/todo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Hyperdo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/packages/todo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@feathers-studio/todo",
3 | "module": "src/index.ts",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "bun run vite"
7 | },
8 | "devDependencies": {
9 | "@types/bun": "latest",
10 | "@types/react": "^19.0.2",
11 | "@types/react-dom": "^19.0.2",
12 | "@types/web": "^0.0.188"
13 | },
14 | "peerDependencies": {
15 | "typescript": "^5.0.0"
16 | },
17 | "dependencies": {
18 | "@hyperactive/hyper": "workspace:*",
19 | "vite": "^6.0.5"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/todo/public/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feathers-studio/hyperactive/cbdcc56ddfee693e41370fa03516b555f4389978/packages/todo/public/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/packages/todo/public/favicon/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feathers-studio/hyperactive/cbdcc56ddfee693e41370fa03516b555f4389978/packages/todo/public/favicon/favicon-96x96.png
--------------------------------------------------------------------------------
/packages/todo/public/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feathers-studio/hyperactive/cbdcc56ddfee693e41370fa03516b555f4389978/packages/todo/public/favicon/favicon.ico
--------------------------------------------------------------------------------
/packages/todo/public/favicon/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Hyperdo",
3 | "short_name": "Hyperdo",
4 | "icons": [
5 | {
6 | "src": "/favicon/web-app-manifest-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png",
9 | "purpose": "maskable"
10 | },
11 | {
12 | "src": "/favicon/web-app-manifest-512x512.png",
13 | "sizes": "512x512",
14 | "type": "image/png",
15 | "purpose": "maskable"
16 | }
17 | ],
18 | "theme_color": "#ffffff",
19 | "background_color": "#ffffff",
20 | "display": "standalone"
21 | }
--------------------------------------------------------------------------------
/packages/todo/public/favicon/web-app-manifest-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feathers-studio/hyperactive/cbdcc56ddfee693e41370fa03516b555f4389978/packages/todo/public/favicon/web-app-manifest-192x192.png
--------------------------------------------------------------------------------
/packages/todo/public/favicon/web-app-manifest-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feathers-studio/hyperactive/cbdcc56ddfee693e41370fa03516b555f4389978/packages/todo/public/favicon/web-app-manifest-512x512.png
--------------------------------------------------------------------------------
/packages/todo/src/App.ts:
--------------------------------------------------------------------------------
1 | import "./styles.css";
2 |
3 | import { div, h2, p, button, form, input, label, svg, li, header, ul } from "@hyperactive/hyper/elements";
4 | import { trust, Member, List, State, renderDOM } from "@hyperactive/hyper";
5 | import type { Window } from "@hyperactive/hyper/dom";
6 |
7 | declare const window: Window;
8 |
9 | // setup phase, write your states anywhere!
10 |
11 | const loading = new State(true);
12 | const todos = new List- ();
13 |
14 | window.addEventListener("load", () => {
15 | const fromMem = JSON.parse(window.localStorage.getItem("todos") || "[]") as Item[];
16 | if (!fromMem.length) {
17 | fromMem.push({
18 | title: "Get started!",
19 | id: window.crypto.randomUUID(),
20 | completed: false,
21 | });
22 | }
23 | fromMem.forEach(todo => todos.append(todo));
24 | loading.set(false);
25 | });
26 |
27 | todos.listen(() => {
28 | window.localStorage.setItem("todos", JSON.stringify(todos.toArray()));
29 | });
30 |
31 | const S = (path: string) => {
32 | return svg(
33 | // @ts-expect-error SVG attributes are not typed correctly?
34 | { width: "32", height: "32", fill: "#000000", viewBox: "0 0 256 256" },
35 | trust(path),
36 | );
37 | };
38 |
39 | export const Hero = (completed: State
, total: State) => {
40 | return header(
41 | div(h2("Tasks done"), p("頑張って〜!")),
42 | div(
43 | { class: "progress" },
44 | p(
45 | completed.to(v => String(v)),
46 | " / ",
47 | total.to(v => String(v)),
48 | ),
49 | ),
50 | );
51 | };
52 |
53 | export function Form(todos: List- ) {
54 | const handleSubmit = (event: any) => {
55 | event.preventDefault();
56 | const title = event.target.todo.value;
57 | if (!title) return;
58 | todos.append({ title, id: window.crypto.randomUUID(), completed: false });
59 | event.target.reset();
60 | };
61 |
62 | return form(
63 | { on: { submit: handleSubmit } },
64 | label(
65 | input({
66 | type: "text",
67 | name: "todo",
68 | id: "todo",
69 | disabled: loading,
70 | placeholder: loading.to(l => (l ? "Loading from local storage..." : "Write your next task")),
71 | autofocus: true,
72 | ref: el => loading.listen(l => l || el.focus()),
73 | }),
74 | ),
75 | button(
76 | { title: "Add task" },
77 | S(
78 | `
`,
79 | ),
80 | ),
81 | );
82 | }
83 |
84 | export interface Item {
85 | id: string;
86 | title: string;
87 | completed: boolean;
88 | }
89 |
90 | export function LocalEditableInput(item: Member- ) {
91 | const value = new State(item.value.title);
92 |
93 | return input({
94 | type: "text",
95 | value,
96 | on: {
97 | blur: () => item.setWith(i => ({ ...i, title: value.value })),
98 | change: e => value.set((e.target as any).value),
99 | },
100 | });
101 | }
102 |
103 | export function Item(item: Member
- ) {
104 | return li(
105 | { class: item.to(i => (i.completed ? "completed" : "")) },
106 | button(
107 | {
108 | title: item.to(i => (i.completed ? "Mark as not completed" : "Mark as completed")),
109 | on: { click: () => item.setWith(i => ({ ...i, completed: !i.completed })) },
110 | },
111 | svg(
112 | // @ts-expect-error SVG attributes are not typed correctly?
113 | { width: "32", height: "32", fill: "#000000", viewBox: "0 0 256 256" },
114 | trust('
'),
115 | ),
116 | ),
117 | LocalEditableInput(item),
118 | button(
119 | { title: "Delete", on: { click: () => item.remove() } },
120 | S(
121 | ' ',
122 | ),
123 | ),
124 | );
125 | }
126 |
127 | export function Home() {
128 | const completed = todos.filter(todo => todo.completed).size;
129 | const total = todos.size;
130 |
131 | return div.container(
132 | //
133 | Hero(completed, total),
134 | Form(todos),
135 | ul(todos.each(todo => Item(todo))),
136 | );
137 | }
138 |
139 | renderDOM(window.document.getElementById("app")!, Home());
140 |
--------------------------------------------------------------------------------
/packages/todo/src/_/diff.tsx:
--------------------------------------------------------------------------------
1 | // // @ts-nocheck
2 |
3 | // type Todo = { id: number; content: string };
4 |
5 | // const Todo = ({ setList, todo, index }: { setList: (list: Todo[]) => void; todo: Todo; index: number }) => {
6 | // const [state, setState] = useState(todo.content);
7 |
8 | // return (
9 | //
10 | // {index + 1}
11 | //
22 | //
23 | // );
24 | // };
25 |
26 | // const App = () => {
27 | // const [todos, setTodos] = useState([
28 | // { id: 1, content: "Hyperactive" },
29 | // { id: 2, content: "Jigza" },
30 | // { id: 3, content: "Telegraf" },
31 | // ]);
32 |
33 | // return (
34 | //
35 | //
36 | // {todos.map((todo, index) => (
37 | //
38 | // ))}
39 | //
40 | //
41 | //
42 | // );
43 | // };
44 |
45 | // createRoot(root).render(App);
46 |
--------------------------------------------------------------------------------
/packages/todo/src/_/hyper.tsx:
--------------------------------------------------------------------------------
1 | // // @ts-nocheck
2 |
3 | // type Todo = { id: number; content: string };
4 |
5 | // const Todo = (todo: ListMember) => {
6 | // const state = new State(todo.content);
7 |
8 | // return (
9 | //
10 | // {todo.index.transform(i => String(i + 1))}
11 | //
22 | //
23 | // );
24 | // };
25 |
26 | // const App = () => {
27 | // const todos = new ListState([
28 | // { id: 1, content: "Hyperactive" },
29 | // { id: 2, content: "Jigza" },
30 | // { id: 3, content: "Telegraf" },
31 | // ]);
32 |
33 | // return (
34 | //
35 | //
36 | // {todos.transform(todo => (
37 | //
38 | // ))}
39 | //
40 | //
41 | //
42 | // );
43 | // };
44 |
45 | // renderDOM(root, );
46 |
--------------------------------------------------------------------------------
/packages/todo/src/_/index.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck test
2 | /* eslint-disable */
3 |
4 | {
5 | // Hyperactive function version (current)
6 |
7 | const Todo = (todo: State<{ id: number; content: string }>) => {
8 | const state = new State(todo.value.content);
9 |
10 | return li(
11 | span(todo.index.transform(i => String(i + 1))),
12 | form(
13 | p(todo.transform(t => t.content)),
14 | input({ type: "text", value: state, on: { change: e => state.update(e.target.value) } }),
15 | button({
16 | on: {
17 | async click() {
18 | await fetch("...", {
19 | method: "PUT",
20 | body: JSON.stringify({ value: state.value }),
21 | });
22 | todo.update({ ...todo, content: state.value });
23 | },
24 | },
25 | }),
26 | ),
27 | );
28 | };
29 |
30 | const App = () => {
31 | const todos = new ListState([
32 | { id: 1, content: "Hyperactive" },
33 | { id: 2, content: "Jigza" },
34 | { id: 3, content: "Telegraf" },
35 | ]);
36 |
37 | return div(
38 | { class: "container" },
39 | ol(todos.transform(Todo)),
40 | button({ on: { click: () => todos.push({ id: todos.length + 1, content: "" }) } }, "Add"),
41 | );
42 | };
43 |
44 | renderDOM(root, App());
45 | }
46 |
47 | {
48 | // Hyperactive JSX version (future)
49 |
50 | type Todo = { id: number; content: string };
51 |
52 | const Todo = ({ list, todo }: { list: ListState; todo: ListMember }) => {
53 | const state = new State(todo.content);
54 |
55 | return (
56 |
57 | {todo.index.transform(i => String(i + 1))}
58 |
69 |
70 | );
71 | };
72 |
73 | const App = () => {
74 | const todos = new ListState([
75 | { id: 1, content: "Hyperactive" },
76 | { id: 2, content: "Jigza" },
77 | { id: 3, content: "Telegraf" },
78 | ]);
79 |
80 | return (
81 |
82 |
83 | {todos.transform(todo => (
84 |
85 | ))}
86 |
87 |
88 |
89 | );
90 | };
91 |
92 | renderDOM(root, );
93 | }
94 |
95 | {
96 | // React version
97 |
98 | const Todo = ({ setList, todo, index }: { setList: (list: { id: number; content: string }[]) => void; todo: { id: number; content: string }; index: number }) => {
99 | const [value, setValue] = useState(todo.content);
100 |
101 | return (
102 |
103 | {index + 1}
104 |
115 |
116 | );
117 | };
118 |
119 | const App = () => {
120 | const [todos, setTodos] = useState([
121 | { id: 1, content: "Hyperactive" },
122 | { id: 2, content: "Jigza" },
123 | { id: 3, content: "Telegraf" },
124 | ]);
125 |
126 | return (
127 |
128 |
129 | {todos.map((todo, index) => (
130 |
131 | ))}
132 |
133 |
134 |
135 | );
136 | };
137 |
138 | createRoot(root).render(App);
139 | }
140 |
--------------------------------------------------------------------------------
/packages/todo/src/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary-colour: #ffaa00;
3 | --secondary-colour: #f0f0f0;
4 | --background-colour: #f0f0f0;
5 | --border-colour: #ccc;
6 | --text-colour: #000000;
7 | --gaps: 0.8rem;
8 | font-size: 18px;
9 | }
10 |
11 | * {
12 | padding: 0;
13 | margin: 0;
14 | box-sizing: border-box;
15 | }
16 |
17 | *:focus-visible {
18 | outline: 2px solid var(--primary-colour);
19 | }
20 |
21 | body {
22 | background-color: var(--background-colour);
23 | }
24 |
25 | .container {
26 | max-width: 40rem;
27 | margin: 0 auto;
28 | padding: 2rem;
29 | display: flex;
30 | flex-direction: column;
31 | gap: var(--gaps);
32 | }
33 |
34 | header {
35 | display: flex;
36 | justify-content: space-between;
37 | align-items: center;
38 | padding: 1.2rem 1.5rem;
39 | border: 1px solid var(--border-colour);
40 | border-radius: 0.5rem;
41 |
42 | & .progress {
43 | display: grid;
44 | place-items: center;
45 | gap: 1rem;
46 | background-color: var(--primary-colour);
47 | width: 5.5rem;
48 | height: 5.5rem;
49 | border-radius: 50%;
50 | font-size: 1.5rem;
51 | color: white;
52 | }
53 | }
54 |
55 | form {
56 | display: flex;
57 | gap: var(--gaps);
58 |
59 | & label {
60 | height: 2.5rem;
61 | width: 100%;
62 | }
63 |
64 | & input {
65 | height: 100%;
66 | width: 100%;
67 | border: none;
68 | border-radius: 0.5rem;
69 | padding: 0.5rem 0.8rem;
70 | font-size: 0.9rem;
71 | background-color: hsl(from var(--background-colour) h s calc(l - 5));
72 | }
73 |
74 | & button {
75 | height: 2.5rem;
76 | width: 2.5rem;
77 | background: var(--primary-colour);
78 | padding: 0.25rem;
79 | border: none;
80 | border-radius: 0.5rem;
81 | cursor: pointer;
82 | display: grid;
83 | place-items: center;
84 |
85 | &:hover {
86 | background: hsl(from var(--primary-colour) h s calc(l + 5));
87 | }
88 |
89 | &:hover svg,
90 | &:focus svg {
91 | rotate: 90deg;
92 | transition: rotate 150ms ease-in-out;
93 | }
94 | }
95 |
96 | & svg {
97 | height: 1.5rem;
98 | width: 1.5rem;
99 | border: none;
100 | fill: white;
101 | }
102 | }
103 |
104 | ul {
105 | display: flex;
106 | flex-direction: column;
107 | gap: var(--gaps);
108 | list-style: none;
109 | width: 100%;
110 |
111 | & li {
112 | display: flex;
113 | border: 1px solid var(--border-colour);
114 | border-radius: 0.5rem;
115 | width: 100%;
116 | position: relative;
117 |
118 | & > * {
119 | padding: 0.6rem;
120 | transition: background 150ms ease-in-out, opacity 150ms ease-in-out;
121 | }
122 |
123 | & > *:hover {
124 | background: hsl(from var(--secondary-colour) h s calc(l - 5));
125 | }
126 |
127 | :focus-visible {
128 | outline: none;
129 | }
130 |
131 | & button {
132 | --size: calc(0.6rem * 2 + 1.25rem);
133 | width: var(--size);
134 | height: var(--size);
135 | min-width: var(--size);
136 | min-height: var(--size);
137 |
138 | background: transparent;
139 | border: none;
140 | cursor: pointer;
141 | display: grid;
142 | place-items: center;
143 |
144 | & svg,
145 | & svg * {
146 | width: 1.25rem;
147 | height: 1.25rem;
148 | transition: fill 150ms ease-in-out, stroke 150ms ease-in-out;
149 | }
150 |
151 | & svg circle {
152 | fill: transparent;
153 | stroke: var(--border-colour);
154 | stroke-width: 0.8rem;
155 | }
156 |
157 | &:hover svg {
158 | fill: var(--primary-colour);
159 | }
160 | }
161 |
162 | & input {
163 | min-height: 100%;
164 | width: 100%;
165 | font-size: 0.8rem;
166 | text-align: left;
167 | overflow: hidden;
168 | text-overflow: ellipsis;
169 | white-space: nowrap;
170 | border: none;
171 | background: transparent;
172 | cursor: pointer;
173 | }
174 |
175 | & input:focus {
176 | background: hsl(from var(--secondary-colour) h s calc(l - 10));
177 | }
178 |
179 | &.completed {
180 | & > * {
181 | opacity: 0.3;
182 | }
183 |
184 | & svg circle {
185 | fill: var(--border-colour);
186 | }
187 | }
188 |
189 | &::after {
190 | content: "";
191 | position: absolute;
192 | top: 50%;
193 | left: -2%;
194 | width: 104%;
195 | height: 1px;
196 | background: hsl(from var(--border-colour) h s calc(l - 15));
197 |
198 | transform: scaleX(0);
199 | transform-origin: left;
200 | transition: transform 400ms ease-in-out;
201 | }
202 |
203 | &.completed::after {
204 | transform: scaleX(1);
205 | }
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/packages/todo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "jsx": "react-jsx",
9 | "allowJs": true,
10 |
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 | // disable ambient types
23 | "types": [],
24 |
25 | // Some stricter flags (disabled by default)
26 | "noUnusedLocals": false,
27 | "noUnusedParameters": false,
28 | "noPropertyAccessFromIndexSignature": false
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/todo/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 |
3 | export default defineConfig({
4 | server: {
5 | port: 10000,
6 | host: true,
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/packages/url/.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/url/README.md:
--------------------------------------------------------------------------------
1 | # @hyperactive/url
2 |
3 | To install dependencies:
4 |
5 | ```bash
6 | bun install
7 | ```
8 |
9 | To run:
10 |
11 | ```bash
12 | bun run src/index.ts
13 | ```
14 |
15 | This project was created using `bun init` in bun v1.1.42. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
16 |
--------------------------------------------------------------------------------
/packages/url/blog/blog.md:
--------------------------------------------------------------------------------
1 | # Designing a spec'd path parser from scratch
2 |
3 | > This blog post is a part of a series of blog posts about the design of Hyperserve, a modern server framework being built for JavaScript. It's part of the broader Hyperactive project, a modern suite of web application development tools.
4 |
5 | ## The problem
6 |
7 | To route requests in a web server, we need to parse a "path spec", so to say, which is a string that describes a path with parameters. You may be familiar with this from frameworks like Express. Express allows named parameters (`:param`) and Regex parameters (`?`, `+`, `*`, and `()`) in path specs. Let's start with the most common one.
8 |
9 | ```ts
10 | app.get("/users/:username", (req, res) => {
11 | // 「 { username: string } 」
12 | res.send(`Hello ${req.params.username}`);
13 | // ^^^^^^
14 | });
15 | ```
16 |
17 | `"/users/:username"` is what I call a path spec. It describes a path that matches the pattern and captures the `username` parameter. If you use TypeScript and have `@types/express` installed, you may have noticed that `req.params` is of type `{ username: string }`. This is because `@types/express` takes advantage of TypeScript's template literal types to model the path spec.
18 |
19 | But there's a big problem here.
20 |
21 | 
22 |
23 | TypeScript cannot parse the string with these limitations. The union formed by the set of these characters is too large, so TypeScript will give up on recursion. So what does @types/express do? It lies. Instead of parsing accepted characters, it delimits the path spec by the chars `/ . -`. Worse yet, it doesn't even support Regex parameters.
24 |
25 | This means that if you have a path spec like `/users/:username`, it will be parsed as `/users/:username` and the `username` parameter will be captured as `username`.
26 |
27 | [Zig]
28 | If we had this it would've been possible and easy
29 | Zig can express it at type level
30 | So what does @types/express do? It lies. It'll accept chars not in the allowed set and it's delimited only by the chars / . -
31 | So if you have /:user@:password/ for some ill-advised reason, it'll show types as
32 |
33 | params: { "user@:password": string }
34 |
35 | and not
36 |
37 | { user: string, password: string }
38 | which runtime Express will give you
39 | There are several other limitations of @types/express, its path parser does not express most of what Express allows
40 | It's not a tradeoff I'm willing to make for Hyperserve, it has to model types accurately. So my parser only delimits by / . -
41 | Some Regex params are supported in Express, but not in @types/express
42 | Okay let's look at the URL spec for paths:
43 | https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
44 | unreserved = ALPHA / DIGIT / "-" / "." / "\_" / "~"
45 |
46 | pct-encoded = "%" HEXDIG HEXDIG
47 |
48 | sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
49 | / "\*" / "+" / "," / ";" / "="
50 |
51 | pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
52 | So basically
53 |
54 | ALPHA | DIGIT | "-" | "." | "\_" | "~" | "!" | "$" | "&" | "'" | "(" | ")" | "\*" | "+" | "," | ";" | "=" | ":" | "@"
55 |
56 | Is all the chars allowed in a path segment, along with %HH
57 |
--------------------------------------------------------------------------------
/packages/url/blog/express.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feathers-studio/hyperactive/cbdcc56ddfee693e41370fa03516b555f4389978/packages/url/blog/express.jpg
--------------------------------------------------------------------------------
/packages/url/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hyperactive/url",
3 | "module": "src/index.ts",
4 | "type": "module",
5 | "scripts": {
6 | "prepublishOnly": "bun run build",
7 | "build": "tsc"
8 | },
9 | "devDependencies": {
10 | "@types/bun": "^1.1.14"
11 | },
12 | "peerDependencies": {
13 | "typescript": "^5.0.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/url/src/HyperURL.ts:
--------------------------------------------------------------------------------
1 | type QuerySpec = Record;
2 |
3 | // Maybe this should allow URLs without domains etc?
4 | export class HyperURL extends URL {
5 | public route: string;
6 | public params: P;
7 | #query: QuerySpec;
8 |
9 | constructor({
10 | url,
11 | route,
12 | params,
13 | spec: { query },
14 | }: {
15 | url: string;
16 | route: string;
17 | params: P;
18 | spec: { query: QuerySpec };
19 | }) {
20 | super(url);
21 |
22 | this.route = route;
23 | this.params = params;
24 | this.#query = query;
25 | }
26 |
27 | get query() {
28 | const spec = this.#query;
29 | return [...this.searchParams.entries()].reduce((q, [k, v]) => {
30 | if (spec[k]) ((q[k] as string[]) || (q[k] = [])).push(v);
31 | else q[k] = v;
32 | return q;
33 | }, {} as Record) as unknown as Q;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/url/src/index.ts:
--------------------------------------------------------------------------------
1 | export { HyperURL } from "./HyperURL.ts";
2 | export { parse } from "./parse.ts";
3 |
--------------------------------------------------------------------------------
/packages/url/src/parse.test.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from "bun:test";
2 | import { parsePathSpec } from "./parse.ts";
3 |
4 | test("parsePathSpec", () => {
5 | const x = parsePathSpec("/x~:x/~:y.:p/asa/:z/");
6 | expect(x).toEqual([
7 | { type: "interjunct", value: "/" },
8 | { type: "literal", value: "x~" },
9 | { type: "param", value: "x" },
10 | { type: "interjunct", value: "/" },
11 | { type: "literal", value: "~" },
12 | { type: "param", value: "y" },
13 | { type: "interjunct", value: "." },
14 | { type: "param", value: "p" },
15 | { type: "interjunct", value: "/" },
16 | { type: "literal", value: "asa" },
17 | { type: "interjunct", value: "/" },
18 | { type: "param", value: "z" },
19 | { type: "interjunct", value: "/" },
20 | ]);
21 | });
22 |
--------------------------------------------------------------------------------
/packages/url/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ES2022"],
5 | "target": "ES2022",
6 | "module": "CommonJS",
7 | "moduleDetection": "force",
8 | "moduleResolution": "node",
9 |
10 | "allowImportingTsExtensions": true,
11 | "rewriteRelativeImportExtensions": true,
12 | "declaration": true,
13 |
14 | // Best practices
15 | "strict": true,
16 | "skipLibCheck": true,
17 | "noFallthroughCasesInSwitch": true,
18 |
19 | // Some stricter flags (disabled by default)
20 | "noUnusedLocals": false,
21 | "noUnusedParameters": false,
22 | "noPropertyAccessFromIndexSignature": false,
23 |
24 | "outDir": "./lib"
25 | },
26 | "include": ["src/**/*"],
27 | "exclude": ["src/**/*.test.ts"]
28 | }
29 |
--------------------------------------------------------------------------------
/packages/web/.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/web/README.md:
--------------------------------------------------------------------------------
1 | # @hyperactive/web
2 |
3 | To install dependencies:
4 |
5 | ```bash
6 | bun install
7 | ```
8 |
9 | To run:
10 |
11 | ```bash
12 | bun run src/index.ts
13 | ```
14 |
15 | This project was created using `bun init` in bun v1.1.42. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
16 |
--------------------------------------------------------------------------------
/packages/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hyperactive/web",
3 | "module": "src/index.ts",
4 | "type": "module",
5 | "devDependencies": {
6 | "@types/bun": "latest"
7 | },
8 | "peerDependencies": {
9 | "typescript": "^5.7.2"
10 | },
11 | "dependencies": {
12 | "@hyperactive/hyper": "workspace:*",
13 | "@hyperactive/serve": "workspace:*",
14 | "marked": "^15.0.5",
15 | "mime-types": "^2.1.35"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/web/public/assets/fonts/GeistMono[wght].woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feathers-studio/hyperactive/cbdcc56ddfee693e41370fa03516b555f4389978/packages/web/public/assets/fonts/GeistMono[wght].woff2
--------------------------------------------------------------------------------
/packages/web/public/assets/fonts/Geist[wght].woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feathers-studio/hyperactive/cbdcc56ddfee693e41370fa03516b555f4389978/packages/web/public/assets/fonts/Geist[wght].woff2
--------------------------------------------------------------------------------
/packages/web/public/assets/fonts/fonts.css:
--------------------------------------------------------------------------------
1 | /* Geist Variable Font */
2 | @font-face {
3 | font-family: "Geist";
4 | src: url("./Geist[wght].woff2") format("woff2");
5 | font-weight: 100 900;
6 | font-style: normal;
7 | font-display: swap;
8 | }
9 |
10 | /* GeistMono Variable Font */
11 | @font-face {
12 | font-family: "GeistMono";
13 | src: url("./GeistMono[wght].woff2") format("woff2");
14 | font-weight: 100 950;
15 | font-style: normal;
16 | font-display: swap;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/web/public/assets/prism/prism.css:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.29.0
2 | https://prismjs.com/download.html#themes=prism-okaidia&languages=css+clike+javascript+bash+typescript&plugins=line-highlight+line-numbers */
3 | code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
4 | pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)}
5 | pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
6 |
--------------------------------------------------------------------------------
/packages/web/public/assets/style.css:
--------------------------------------------------------------------------------
1 | @import url("./fonts/fonts.css");
2 | @import url("./prism/prism.css");
3 |
4 | :root {
5 | --brand-gradient: linear-gradient(to right, #ff6a00, #ffaa00);
6 | --primary-colour: #ff6a00;
7 | --secondary-colour: #f0f0f0;
8 | --background-colour: #ffffff;
9 | --border-colour: #ccc;
10 | --text-colour: #000000;
11 | --gaps: 0.8rem;
12 | font-size: 18px;
13 | scroll-behavior: smooth;
14 | }
15 |
16 | @media (max-width: 768px) {
17 | :root {
18 | font-size: 16px;
19 | }
20 | }
21 |
22 | * {
23 | padding: 0;
24 | margin: 0;
25 | box-sizing: border-box;
26 | font-family: "Geist", sans-serif;
27 | }
28 |
29 | *:focus-visible {
30 | outline: 2px solid var(--primary-colour);
31 | }
32 |
33 | body {
34 | min-height: 100vh;
35 | max-width: 100vw;
36 | overflow-x: hidden;
37 | background: var(--background-colour);
38 | padding-block: 5.5rem;
39 | }
40 |
41 | button,
42 | .button {
43 | width: max-content;
44 | display: inline-block;
45 | cursor: pointer;
46 | padding: 0.6rem 1rem;
47 | border: 2px solid var(--text-colour);
48 | text-decoration: none;
49 | text-transform: uppercase;
50 | color: var(--text-colour);
51 | font-size: 1rem;
52 |
53 | position: relative;
54 | z-index: 1;
55 | transition: color 500ms ease;
56 |
57 | &::before,
58 | &::after {
59 | content: "";
60 | position: absolute;
61 | inset: 0;
62 | border-radius: inherit;
63 | transition: opacity 500ms ease;
64 | }
65 |
66 | &::before {
67 | z-index: -2;
68 | background-color: var(--background-colour);
69 | }
70 |
71 | &::after {
72 | z-index: -1;
73 | background-color: var(--text-colour);
74 | transform: scaleX(0);
75 | transform-origin: left;
76 | transition: transform 500ms ease;
77 | }
78 |
79 | &:hover {
80 | color: var(--background-colour);
81 | &::after {
82 | transform: scaleX(1);
83 | }
84 | }
85 | }
86 |
87 | .container {
88 | width: 100%;
89 | max-width: 100rem;
90 | margin: 0 auto;
91 | padding-inline: 3.5rem;
92 | display: flex;
93 | flex-direction: column;
94 | gap: var(--gaps);
95 | }
96 |
97 | .text_gradient {
98 | background-image: var(--brand-gradient);
99 | background-clip: text;
100 | -webkit-text-fill-color: transparent;
101 | }
102 |
103 | .marker {
104 | font-size: 0.8rem;
105 | font-weight: 200;
106 | line-height: 1;
107 | padding: 0.2rem 0.5rem 0.24rem;
108 | border-radius: 0.5rem;
109 |
110 | border-width: 1px;
111 | border-style: solid;
112 | border-color: transparent;
113 |
114 | background-image: linear-gradient(white, white), var(--brand-gradient);
115 | background-origin: border-box;
116 | background-clip: padding-box, border-box;
117 |
118 | cursor: pointer;
119 |
120 | position: relative;
121 | z-index: 1;
122 |
123 | &::before {
124 | content: "";
125 | position: absolute;
126 | inset: 0;
127 | border-radius: inherit;
128 | background-image: var(--brand-gradient);
129 | z-index: -1;
130 | opacity: 0;
131 | transition: opacity 500ms ease;
132 | }
133 |
134 | &:hover {
135 | &::before {
136 | opacity: 1;
137 | }
138 | }
139 |
140 | & .tooltip {
141 | opacity: 0;
142 | position: absolute;
143 | top: calc(100% + 0.5rem);
144 | right: 0%;
145 | width: max-content;
146 | max-width: 10rem;
147 | transition: opacity 500ms ease;
148 | pointer-events: none;
149 | line-height: 1.2;
150 | font-weight: 500;
151 | text-align: right;
152 | }
153 |
154 | &:hover {
155 | & .tooltip {
156 | opacity: 1;
157 | }
158 | }
159 | }
160 |
161 | header {
162 | display: flex;
163 | justify-content: space-between;
164 | align-items: center;
165 | width: 100%;
166 | }
167 |
168 | header .logo svg {
169 | width: 3.5rem;
170 | height: auto;
171 | }
172 |
173 | & code {
174 | &,
175 | & * {
176 | font-family: "GeistMono", monospace;
177 | font-size: 0.85rem;
178 | }
179 | padding: 0.1rem 0.25rem;
180 | background-color: hsl(from var(--border-colour) h s calc(l + 10));
181 | border-radius: 0.2rem;
182 | }
183 |
184 | pre {
185 | overflow-x: auto;
186 | max-width: 40rem;
187 | background-color: rgb(0 0 0 / 0.9);
188 | color: rgb(255 255 255 / 0.9);
189 | padding: 1.5rem 1.5rem;
190 | border-radius: 0.4rem;
191 |
192 | & code {
193 | background: none;
194 | padding: 0;
195 | /* font-size: inherit; */
196 | }
197 | }
198 |
199 | .hero {
200 | height: 85vh;
201 | & * {
202 | font-family: "GeistMono", monospace;
203 | }
204 |
205 | & main {
206 | display: flex;
207 | flex-direction: column;
208 | justify-content: center;
209 | /* padding-top: 10.5rem; */
210 | height: 100%;
211 |
212 | * {
213 | margin: 0;
214 | line-height: 1.1;
215 | }
216 |
217 | h1 {
218 | font-size: 3.4rem;
219 | text-transform: lowercase;
220 | font-weight: 300;
221 | }
222 |
223 | p {
224 | font-size: 2.1rem;
225 | font-weight: 300;
226 | max-width: 35rem;
227 | }
228 |
229 | .button {
230 | margin-top: 4rem;
231 | }
232 | }
233 | }
234 |
235 | #docs article {
236 | max-width: 50rem;
237 | overflow-x: auto;
238 |
239 | & > * {
240 | margin-top: 1.2rem;
241 | }
242 |
243 | & p {
244 | line-height: 1.5;
245 | }
246 |
247 | & h2:not(:first-child) {
248 | margin-top: 3rem;
249 | position: relative;
250 |
251 | &::before {
252 | content: "";
253 | position: absolute;
254 | left: 0;
255 | top: -1.2rem;
256 | height: 1px;
257 | width: 100%;
258 | background-color: var(--border-colour);
259 | }
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/packages/web/src/content/docs.md:
--------------------------------------------------------------------------------
1 | ## Installation
2 |
3 | If you use npm:
4 |
5 | ```bash
6 | npm install https://gethyper.dev
7 | ```
8 |
9 | Or, if you use pnpm, yarn, or bun:
10 |
11 | ```bash
12 | pnpm add https://gethyper.dev
13 | ```
14 |
15 | ## Basic Usage
16 |
17 | ### On the Server
18 |
19 | `renderHTML` is used to render a Hyperactive component to a HTML string. Use this like a template engine.
20 |
21 | ```TypeScript
22 | import { renderHTML } from "@hyperactive/hyper";
23 | import { html, head, body, section, img, h1, title, link } from "@hyperactive/hyper/elements";
24 |
25 | const GreeterPage = (name: string) =>
26 | renderHTML(
27 | html(
28 | head(
29 | title("Greeter"),
30 | link({ rel: "stylesheet", href: "/assets/style.css" }),
31 | ),
32 | body(
33 | section(
34 | { class: "container" },
35 | img({ src: "/hero.jpg" }),
36 | h1("Hello ", name),
37 | ),
38 | ),
39 | ),
40 | );
41 |
42 | Bun.serve({
43 | port: 3000,
44 | fetch(req) {
45 | if (req.url === "/") {
46 | return new Response(
47 | GreeterPage("World"),
48 | { headers: { "Content-Type": "text/html" } },
49 | );
50 | }
51 | },
52 | });
53 | ```
54 |
55 | ### In the Browser
56 |
57 | `renderDOM` is used to render a Hyperactive component to the DOM.
58 |
59 | This is a truly reactive system, where the DOM is updated whenever relevant state changes. Unlike frameworks like React, Hyperactive doesn't use a virtual DOM. Instead, it remembers what state changes affect which DOM nodes, and only updates the DOM nodes that need to be updated. Unlike Svelte, Hyperactive doesn't use a compiler. Instead, it uses a runtime library that is designed to be as small and fast as possible. This is the ideal: a Hyperscript that is more convenient as React, fast as Svelte, and as reactive as Solid.
60 |
61 | [](https://npmjs.com/package/@types/web)
62 |
63 | Please install `@types/web` to use Hyperactive in the browser. Your package manager will automatically install the correct version of `@types/web` for you by default. See the [versions](./docs/versions.md) table for the correct version of `@types/web` for each version of Hyperactive.
64 |
65 | ```bash
66 | bun add @types/web
67 | ```
68 |
69 | ```TypeScript
70 | import { State, renderDOM } from "@hyperactive/hyper";
71 | import { div, p, button } from "@hyperactive/hyper/elements";
72 |
73 | const count = new State(0);
74 |
75 | const root = document.getElementById("root");
76 |
77 | renderDOM(
78 | root,
79 | div(
80 | p("You clicked ", count, " times"),
81 | button(
82 | { on: { click: () => count.set(count.value + 1) } },
83 | "Increment"
84 | ),
85 | ),
86 | );
87 |
88 | ```
89 |
90 | Notice how there are no components, nor is state boxed inside of them. Instead, state is just a plain variable that can be used anywhere. Components can still be used for convenience and to encapsulate state, but they disappear while Hyperactive renders them. Hyperactive only remembers the DOM node to update. In this example, the `div` element is updated when the `count` state changes. The rest of the tree is never updated, so Hyperactive doesn't manage them.
91 |
92 | Let's refactor this example to use a component.
93 |
94 | ```TypeScript
95 | const Counter = () => {
96 | const count = new State(0);
97 |
98 | return div(
99 | p("You clicked ", count, " times"),
100 | button(
101 | { on: { click: () => count.set(count.value + 1) } },
102 | "Increment"
103 | ),
104 | );
105 | };
106 |
107 | renderDOM(root, Counter());
108 | ```
109 |
110 |
111 | But where is my JSX?!
112 |
113 | Hyperactive doesn't use JSX. Instead, we use a simple, declarative JavaScript syntax that is easy to understand and write. In the future, we may consider adding JSX support, and we welcome any contributions in this direction. Ideally we may only need to adapt our `h` function and add `Fragment` support.
114 |
115 |
116 |
--------------------------------------------------------------------------------
/packages/web/src/index.ts:
--------------------------------------------------------------------------------
1 | import { serve } from "bun";
2 | import { join, extname } from "node:path";
3 | import { lookup } from "mime-types";
4 | import { generateETagFromFile, html, redirect, seconds } from "@hyperactive/serve/utils";
5 | import { Home } from "./pages/home";
6 | import { Layout } from "./pages/layout";
7 |
8 | const port = process.env.PORT || 3000;
9 | const S3_ASSETS_ROOT = "https://gethyper.s3.fr-par.scw.cloud/assets";
10 | const ASSETS_ROOT = join(import.meta.dir, "../public/assets");
11 |
12 | // User agent strings
13 | // 'user-agent': 'npm/10.1.0 node/v20.8.1 linux x64 workspaces/false'
14 | // 'user-agent': 'pnpm/8.10.5 npm/? node/v20.8.1 linux x64'
15 | // 'user-agent': 'yarn/1.22.21 npm/? node/v20.8.1 linux x64'
16 | // 'user-agent': 'Bun/1.0.14'
17 |
18 | const layout = Layout("Hyperactive - a powerful toolbox for modern web application development");
19 |
20 | serve({
21 | port,
22 | async fetch(req) {
23 | const url = new URL(req.url);
24 | const method = req.method;
25 | const pathname = url.pathname;
26 | const ua = req.headers.get("user-agent");
27 | const isNPMLike = ua?.includes("npm") || ua?.includes("Bun");
28 |
29 | if (isNPMLike) {
30 | if (pathname === "/")
31 | return new Response(null, {
32 | status: 301,
33 | headers: { location: "https://gethyper.s3.fr-par.scw.cloud/pkg/hyperactive-hyper-2.0.0-beta.1.tgz" },
34 | });
35 | }
36 |
37 | if (method === "GET") {
38 | // if (pathname.startsWith("/fonts/")) return redirect(S3_ASSETS_ROOT + pathname);
39 |
40 | if (url.pathname.startsWith("/assets/")) {
41 | const assetPath = join(ASSETS_ROOT, url.pathname.slice("/assets/".length));
42 | if (!assetPath.startsWith(ASSETS_ROOT)) {
43 | return new Response("Access Denied", { status: 403 });
44 | }
45 | return new Response(Bun.file(assetPath), {
46 | headers: {
47 | "Content-Type": lookup(extname(assetPath)) || "application/octet-stream",
48 | // "Cache-Control": `public, max-age=${seconds(10)}`,
49 | // "ETag": await generateETagFromFile(assetPath),
50 | },
51 | });
52 | }
53 |
54 | if (pathname === "/") return html(layout(await Home()));
55 | }
56 |
57 | return new Response("Not found", { status: 404 });
58 | },
59 | });
60 |
61 | console.log(`Server is running on port ${port}`);
62 |
--------------------------------------------------------------------------------
/packages/web/src/pages/home.ts:
--------------------------------------------------------------------------------
1 | import { join } from "node:path";
2 | import { List, trust } from "@hyperactive/hyper";
3 | import { a, article, br, div, h1, header, main, p, section, span } from "@hyperactive/hyper/elements";
4 | import { marked } from "marked";
5 |
6 | const logo = trust(await Bun.file(join(import.meta.dir, "../../../../docs/h(⚡️).svg")).text());
7 |
8 | const docs = join(import.meta.dir, "../content/docs.md");
9 |
10 | export async function Home() {
11 | const content = trust(await marked.parse(await Bun.file(docs).text()));
12 |
13 | return new List([
14 | section.container.hero(
15 | header(
16 | span.logo(logo),
17 | div.marker("beta", span.tooltip("Expect bugs!", br(), "These docs are a work in progress!")),
18 | ),
19 | main(
20 | h1.text_gradient("Hyperactive"),
21 | p("is a powerful toolbox for web application development"),
22 | a.button({ href: "#docs" }, "Get Started"),
23 | ),
24 | ),
25 | section.container["#docs"](article(content)),
26 | ]);
27 | }
28 |
--------------------------------------------------------------------------------
/packages/web/src/pages/layout.ts:
--------------------------------------------------------------------------------
1 | import { renderHTML, type HyperNodeish } from "@hyperactive/hyper";
2 | import { body, head, html, link, meta, script, title as title_tag } from "@hyperactive/hyper/elements";
3 |
4 | export function Layout(title: string, style = "/assets/style.css") {
5 | return function (children: HyperNodeish) {
6 | return renderHTML(
7 | html(
8 | head(
9 | meta({ charset: "utf-8" }),
10 | meta({ name: "viewport", content: "width=device-width, initial-scale=1.0" }),
11 | title_tag(title),
12 | link({ rel: "stylesheet", href: style }),
13 | ),
14 | body(children, script({ src: "/assets/prism/prism.js" })),
15 | ),
16 | );
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/packages/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext", "DOM"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "jsx": "react-jsx",
9 | "allowJs": true,
10 |
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 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "allowJs": true,
9 |
10 | // Bundler mode
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "verbatimModuleSyntax": true,
14 | "noEmit": true,
15 |
16 | // Best practices
17 | "strict": true,
18 | "skipLibCheck": true,
19 | "noFallthroughCasesInSwitch": true,
20 | "types": [],
21 |
22 | // Some stricter flags (disabled by default)
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "noPropertyAccessFromIndexSignature": false
26 | }
27 | }
28 |
--------------------------------------------------------------------------------