htmx-router 1.0.0-alpha.4 → 1.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{bin/util/css.js → css.js} +1 -1
- package/dynamic.d.ts +5 -0
- package/{bin/util/dynamic.js → dynamic.js} +20 -7
- package/{bin/util/endpoint.d.ts → endpoint.d.ts} +2 -2
- package/{bin/util/endpoint.js → endpoint.js} +3 -1
- package/event-source.d.ts +26 -0
- package/event-source.js +123 -0
- package/example/eventdim-react/package.json +67 -0
- package/example/eventdim-react/server.js +90 -0
- package/example/island-react/global.d.ts +8 -0
- package/example/island-react/package.json +38 -0
- package/example/island-react/server.js +58 -0
- package/global.d.ts +7 -0
- package/index.d.ts +19 -0
- package/index.js +2 -0
- package/internal/cli/config.d.ts +13 -0
- package/internal/cli/config.js +11 -0
- package/internal/cli/index.js +15 -0
- package/internal/client.d.ts +1 -0
- package/{bin/client/entry.js → internal/client.js} +3 -1
- package/internal/compile/manifest.d.ts +1 -0
- package/internal/compile/manifest.js +178 -0
- package/internal/compile/router.d.ts +1 -0
- package/internal/compile/router.js +51 -0
- package/internal/component/dynamic.d.ts +4 -0
- package/internal/component/dynamic.js +18 -0
- package/internal/component/head.d.ts +5 -0
- package/internal/component/head.js +22 -0
- package/internal/component/scripts.d.ts +4 -0
- package/internal/component/scripts.js +23 -0
- package/{bin/client → internal}/mount.js +15 -9
- package/internal/request/http.d.ts +10 -0
- package/internal/request/http.js +61 -0
- package/{bin → internal}/request/index.d.ts +3 -3
- package/internal/request/index.js +8 -0
- package/{bin → internal}/request/native.d.ts +2 -2
- package/{bin → internal}/request/native.js +12 -14
- package/{bin/helper.d.ts → internal/util.d.ts} +2 -0
- package/{bin/helper.js → internal/util.js} +15 -0
- package/package.json +9 -5
- package/readme.md +2 -214
- package/{bin/request → request}/http.d.ts +1 -1
- package/{bin/request → request}/http.js +22 -4
- package/request/index.d.ts +13 -0
- package/request/index.js +3 -0
- package/request/native.d.ts +9 -0
- package/request/native.js +46 -0
- package/response.d.ts +13 -0
- package/{bin/response.js → response.js} +25 -12
- package/{bin/router.d.ts → router.d.ts} +12 -10
- package/{bin/router.js → router.js} +62 -48
- package/shell.d.ts +120 -0
- package/shell.js +253 -0
- package/{bin/util → util}/parameters.d.ts +0 -3
- package/{bin/util → util}/parameters.js +0 -3
- package/{bin/util → util}/path-builder.js +2 -0
- package/util/route.d.ts +2 -0
- package/util/route.js +58 -0
- package/vite/bundle-splitter.d.ts +4 -0
- package/vite/bundle-splitter.js +26 -0
- package/vite/client-island.d.ts +4 -0
- package/vite/client-island.js +14 -0
- package/vite/code-splitting.d.ts +4 -0
- package/vite/code-splitting.js +14 -0
- package/vite/index.d.ts +3 -0
- package/vite/index.js +3 -0
- package/vite/router.d.ts +2 -0
- package/vite/router.js +29 -0
- package/bin/cli/config.d.ts +0 -10
- package/bin/cli/config.js +0 -4
- package/bin/cli/index.js +0 -72
- package/bin/client/entry.d.ts +0 -1
- package/bin/client/index.d.ts +0 -7
- package/bin/client/index.js +0 -126
- package/bin/client/watch.d.ts +0 -1
- package/bin/client/watch.js +0 -11
- package/bin/index.d.ts +0 -9
- package/bin/index.js +0 -8
- package/bin/request/index.js +0 -6
- package/bin/response.d.ts +0 -4
- package/bin/types.d.ts +0 -10
- package/bin/types.js +0 -1
- package/bin/util/dynamic.d.ts +0 -8
- package/bin/util/event-source.d.ts +0 -16
- package/bin/util/event-source.js +0 -85
- package/bin/util/index.d.ts +0 -1
- package/bin/util/index.js +0 -7
- package/bin/util/shell.d.ts +0 -32
- package/bin/util/shell.js +0 -8
- /package/{bin/util/cookies.d.ts → cookies.d.ts} +0 -0
- /package/{bin/util/cookies.js → cookies.js} +0 -0
- /package/{bin/util/css.d.ts → css.d.ts} +0 -0
- /package/{bin → internal}/cli/index.d.ts +0 -0
- /package/{bin/util → internal}/hash.d.ts +0 -0
- /package/{bin/util → internal}/hash.js +0 -0
- /package/{bin/client → internal}/mount.d.ts +0 -0
- /package/{bin/util → util}/path-builder.d.ts +0 -0
package/dynamic.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { GenericContext } from "./router.js";
|
|
2
|
+
type Loader<T> = (ctx: GenericContext, params: T) => Promise<JSX.Element | Response>;
|
|
3
|
+
export declare function DynamicReference<T extends Record<string, string>>(loader: Loader<T>, params?: T): string;
|
|
4
|
+
export declare function _resolve(fragments: string[], ctx: GenericContext): Promise<Response | null>;
|
|
5
|
+
export {};
|
|
@@ -1,20 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { QuickHash } from "../util/hash.js";
|
|
1
|
+
import { ServerOnlyWarning } from "./internal/util.js";
|
|
2
|
+
ServerOnlyWarning("dynamic-ref");
|
|
3
|
+
import { QuickHash } from "./internal/util.js";
|
|
5
4
|
const registry = new Map();
|
|
6
5
|
const index = new Map();
|
|
7
|
-
|
|
6
|
+
function Register(load) {
|
|
8
7
|
const existing = index.get(load);
|
|
9
8
|
if (existing)
|
|
10
9
|
return existing;
|
|
11
10
|
const hash = QuickHash(String(load));
|
|
12
11
|
const name = `${encodeURIComponent(load.name)}-${hash}`;
|
|
13
12
|
registry.set(name, load);
|
|
14
|
-
const url = `/_/dynamic/${name}
|
|
13
|
+
const url = `/_/dynamic/${name}`;
|
|
15
14
|
index.set(load, url);
|
|
16
15
|
return url;
|
|
17
16
|
}
|
|
17
|
+
export function DynamicReference(loader, params) {
|
|
18
|
+
let url = Register(loader);
|
|
19
|
+
if (params) {
|
|
20
|
+
const query = new URLSearchParams();
|
|
21
|
+
if (params)
|
|
22
|
+
for (const key in params)
|
|
23
|
+
query.set(key, params[key]);
|
|
24
|
+
url += "?" + query.toString();
|
|
25
|
+
}
|
|
26
|
+
return url;
|
|
27
|
+
}
|
|
18
28
|
export async function _resolve(fragments, ctx) {
|
|
19
29
|
if (!fragments[2])
|
|
20
30
|
return null;
|
|
@@ -25,5 +35,8 @@ export async function _resolve(fragments, ctx) {
|
|
|
25
35
|
for (const [key, value] of ctx.url.searchParams)
|
|
26
36
|
props[key] = value;
|
|
27
37
|
ctx.headers.set("X-Partial", "true");
|
|
28
|
-
|
|
38
|
+
const res = await endpoint(ctx, props);
|
|
39
|
+
if (res instanceof Response)
|
|
40
|
+
return res;
|
|
41
|
+
return ctx.render(res);
|
|
29
42
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { GenericContext } from "./router.js";
|
|
2
|
+
import type { RenderFunction } from "./index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Create a route-less endpoint
|
|
5
5
|
* The name is optional and will be inferred from the function if not given (helpful for network waterfalls)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper for Server-Sent-Events, with auto close on SIGTERM and SIGHUP messages
|
|
3
|
+
* Includes a keep alive empty packet sent every 30sec (because Chrome implodes at 120sec, and can be unreliable at 60sec)
|
|
4
|
+
*/
|
|
5
|
+
export declare class EventSource {
|
|
6
|
+
private controller;
|
|
7
|
+
private timer;
|
|
8
|
+
private state;
|
|
9
|
+
readonly response: Response;
|
|
10
|
+
readonly url: string;
|
|
11
|
+
constructor(request: Request, keepAlive?: number);
|
|
12
|
+
get readyState(): number;
|
|
13
|
+
private sendBytes;
|
|
14
|
+
private sendText;
|
|
15
|
+
private keepAlive;
|
|
16
|
+
dispatch(type: string, data: string): boolean;
|
|
17
|
+
close(unlink?: boolean): boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare class EventSourceSet extends Set<EventSource> {
|
|
20
|
+
/** Send update to all EventSources, auto closing failed dispatches */
|
|
21
|
+
dispatch(type: string, data: string): void;
|
|
22
|
+
/** Cull all closed connections */
|
|
23
|
+
cull(): void;
|
|
24
|
+
/** Close all connections */
|
|
25
|
+
closeAll(): void;
|
|
26
|
+
}
|
package/event-source.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { ServerOnlyWarning } from "./internal/util.js";
|
|
2
|
+
ServerOnlyWarning("event-source");
|
|
3
|
+
/**
|
|
4
|
+
* Helper for Server-Sent-Events, with auto close on SIGTERM and SIGHUP messages
|
|
5
|
+
* Includes a keep alive empty packet sent every 30sec (because Chrome implodes at 120sec, and can be unreliable at 60sec)
|
|
6
|
+
*/
|
|
7
|
+
export class EventSource {
|
|
8
|
+
controller;
|
|
9
|
+
timer;
|
|
10
|
+
state;
|
|
11
|
+
response;
|
|
12
|
+
url; // just to make it polyfill
|
|
13
|
+
constructor(request, keepAlive = 30_000) {
|
|
14
|
+
this.controller = null;
|
|
15
|
+
this.state = 0;
|
|
16
|
+
this.url = request.url;
|
|
17
|
+
const stream = new ReadableStream({
|
|
18
|
+
start: (c) => { this.controller = c; this.state = 1; },
|
|
19
|
+
cancel: () => { this.close(); }
|
|
20
|
+
});
|
|
21
|
+
request.signal.addEventListener('abort', () => this.close());
|
|
22
|
+
this.response = new Response(stream, { headers });
|
|
23
|
+
this.timer = setInterval(() => this.keepAlive(), keepAlive);
|
|
24
|
+
register.push(this);
|
|
25
|
+
}
|
|
26
|
+
get readyState() {
|
|
27
|
+
return this.state;
|
|
28
|
+
}
|
|
29
|
+
sendBytes(chunk) {
|
|
30
|
+
if (!this.controller)
|
|
31
|
+
return false;
|
|
32
|
+
try {
|
|
33
|
+
this.controller.enqueue(chunk);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.error(e);
|
|
38
|
+
this.close(); // unbind on failure
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
sendText(chunk) {
|
|
43
|
+
return this.sendBytes(encoder.encode(chunk));
|
|
44
|
+
}
|
|
45
|
+
keepAlive() {
|
|
46
|
+
return this.sendText("\n\n");
|
|
47
|
+
}
|
|
48
|
+
dispatch(type, data) {
|
|
49
|
+
return this.sendText(`event: ${type}\ndata: ${data}\n\n`);
|
|
50
|
+
}
|
|
51
|
+
close(unlink = true) {
|
|
52
|
+
if (this.state === 2)
|
|
53
|
+
return false;
|
|
54
|
+
if (unlink) {
|
|
55
|
+
const i = register.indexOf(this);
|
|
56
|
+
if (i !== -1)
|
|
57
|
+
register.splice(i, 1);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
this.controller?.close();
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
console.error(e);
|
|
64
|
+
this.controller = null;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
// Cleanup
|
|
68
|
+
if (this.timer)
|
|
69
|
+
clearInterval(this.timer);
|
|
70
|
+
this.controller = null;
|
|
71
|
+
this.state = 2;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export class EventSourceSet extends Set {
|
|
76
|
+
/** Send update to all EventSources, auto closing failed dispatches */
|
|
77
|
+
dispatch(type, data) {
|
|
78
|
+
for (const stream of this) {
|
|
79
|
+
if (stream.readyState === 0)
|
|
80
|
+
continue; // skip initializing
|
|
81
|
+
const success = stream.dispatch(type, data);
|
|
82
|
+
if (!success)
|
|
83
|
+
this.delete(stream);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Cull all closed connections */
|
|
87
|
+
cull() {
|
|
88
|
+
for (const stream of this) {
|
|
89
|
+
if (stream.readyState !== 2)
|
|
90
|
+
continue;
|
|
91
|
+
this.delete(stream);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Close all connections */
|
|
95
|
+
closeAll() {
|
|
96
|
+
for (const stream of this)
|
|
97
|
+
stream.close();
|
|
98
|
+
this.clear();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// global for easy reuse
|
|
102
|
+
const encoder = new TextEncoder();
|
|
103
|
+
const headers = new Headers();
|
|
104
|
+
// Chunked encoding with immediate forwarding by proxies (i.e. nginx)
|
|
105
|
+
headers.set("X-Accel-Buffering", "no");
|
|
106
|
+
headers.set("Transfer-Encoding", "chunked");
|
|
107
|
+
headers.set("Content-Type", "text/event-stream");
|
|
108
|
+
headers.set("Keep-Alive", "timeout=120"); // the maximum keep alive chrome shouldn't ignore
|
|
109
|
+
headers.set("Connection", "keep-alive");
|
|
110
|
+
// Auto close all SSE streams when shutdown requested
|
|
111
|
+
// Without this graceful shutdowns will hang indefinitely
|
|
112
|
+
const register = new Array();
|
|
113
|
+
function CloseAll() {
|
|
114
|
+
for (const connection of register)
|
|
115
|
+
connection.close(false); // don't waste time unregistering
|
|
116
|
+
}
|
|
117
|
+
if (process) {
|
|
118
|
+
process.on('SIGTERM', CloseAll);
|
|
119
|
+
process.on('SIGHUP', CloseAll);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.warn("htmx-router's EventSource has been unsafely loaded on the client");
|
|
123
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eventdim-react",
|
|
3
|
+
"private": "true",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"prepare": "npx htmx-router && prisma generate && prisma migrate deploy",
|
|
9
|
+
"docker": "docker compose up -d",
|
|
10
|
+
"dev": "node ./server.js",
|
|
11
|
+
"build": "run-s build:*",
|
|
12
|
+
"build:router": "npx htmx-router",
|
|
13
|
+
"build:prisma": "npx prisma generate",
|
|
14
|
+
"build:client": "vite build",
|
|
15
|
+
"build:server": "vite build --ssr app/entry.server.ts --outDir dist/server",
|
|
16
|
+
"validate": "run-s validate:*",
|
|
17
|
+
"validate:typecheck": "tsc --noEmit",
|
|
18
|
+
"validate:lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
|
19
|
+
"preview": "cross-env NODE_ENV=production node ./server.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"description": "",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
|
27
|
+
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
|
28
|
+
"@fortawesome/react-fontawesome": "^0.2.2",
|
|
29
|
+
"@prisma/client": "^6.1.0",
|
|
30
|
+
"bcryptjs": "^2.4.3",
|
|
31
|
+
"cbor2": "^1.8.0",
|
|
32
|
+
"cross-env": "^7.0.3",
|
|
33
|
+
"dotenv": "^16.4.7",
|
|
34
|
+
"express": "^4.21.2",
|
|
35
|
+
"htmx-router": "^1.0.0-alpha.5",
|
|
36
|
+
"morgan": "^1.10.0",
|
|
37
|
+
"react": "^19.0.0",
|
|
38
|
+
"react-dom": "^19.0.0",
|
|
39
|
+
"tiny-invariant": "^1.3.3",
|
|
40
|
+
"zxcvbn": "^4.4.2"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/bcryptjs": "^2.4.6",
|
|
44
|
+
"@types/express": "^4.17.21",
|
|
45
|
+
"@types/nodemailer": "^6.4.15",
|
|
46
|
+
"@types/react": "^18.2.20",
|
|
47
|
+
"@types/react-dom": "^18.2.7",
|
|
48
|
+
"@types/zxcvbn": "^4.4.4",
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
|
50
|
+
"@typescript-eslint/parser": "^6.7.4",
|
|
51
|
+
"eslint": "^8.38.0",
|
|
52
|
+
"eslint-import-resolver-typescript": "^3.6.1",
|
|
53
|
+
"eslint-plugin-import": "^2.28.1",
|
|
54
|
+
"eslint-plugin-jsx-a11y": "^6.7.1",
|
|
55
|
+
"eslint-plugin-react": "^7.33.2",
|
|
56
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
57
|
+
"npm-run-all": "^4.1.5",
|
|
58
|
+
"prisma": "^6.1.0",
|
|
59
|
+
"typed-htmx": "^0.3.1",
|
|
60
|
+
"typescript": "^5.5.4",
|
|
61
|
+
"vite-tsconfig-paths": "^5.1.3",
|
|
62
|
+
"vite": "^6.0.1"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=20.0.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
import 'dotenv/config'
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { createRequestHandler } from 'htmx-router';
|
|
6
|
+
import { renderToString } from 'react-dom/server';
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import morgan from "morgan";
|
|
9
|
+
|
|
10
|
+
const port = process.env.PORT || 3000;
|
|
11
|
+
const app = express();
|
|
12
|
+
|
|
13
|
+
const viteDevServer =
|
|
14
|
+
process.env.NODE_ENV === "production"
|
|
15
|
+
? null
|
|
16
|
+
: await import("vite").then((vite) =>
|
|
17
|
+
vite.createServer({
|
|
18
|
+
server: { middlewareMode: true },
|
|
19
|
+
appType: 'custom'
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
app.use(
|
|
24
|
+
viteDevServer
|
|
25
|
+
? viteDevServer.middlewares
|
|
26
|
+
: express.static("./dist/client")
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// logging
|
|
30
|
+
app.use(morgan("tiny"));
|
|
31
|
+
|
|
32
|
+
const build = viteDevServer
|
|
33
|
+
? () => viteDevServer.ssrLoadModule('./app/entry.server.ts')
|
|
34
|
+
: await import('./dist/server/entry.server.js');
|
|
35
|
+
|
|
36
|
+
app.use('*', createRequestHandler.http({
|
|
37
|
+
build, viteDevServer,
|
|
38
|
+
render: (res) => {
|
|
39
|
+
const headers = new Headers();
|
|
40
|
+
headers.set("Content-Type", "text/html; charset=UTF-8");
|
|
41
|
+
headers.set("Cache-Control", "no-cache");
|
|
42
|
+
|
|
43
|
+
const stream = renderToString(res);
|
|
44
|
+
return new Response(stream, { headers });
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Start http server
|
|
49
|
+
app.listen(port, () => {
|
|
50
|
+
console.log(`Server started at http://localhost:${port}`)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
// Reload pages on file change
|
|
55
|
+
if (viteDevServer) {
|
|
56
|
+
const focus = path.resolve("./app");
|
|
57
|
+
viteDevServer.watcher.on('change', (file) => {
|
|
58
|
+
if (!file.startsWith(focus)) return;
|
|
59
|
+
console.log(`File changed: ${path.relative("./app", file)}`);
|
|
60
|
+
|
|
61
|
+
console.log('Triggering full page reload');
|
|
62
|
+
viteDevServer.ws.send({ type: 'full-reload' });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const shutdown = () => {
|
|
67
|
+
console.log("Shutting down server...");
|
|
68
|
+
|
|
69
|
+
// Close the server gracefully
|
|
70
|
+
server.close((err) => {
|
|
71
|
+
if (err) {
|
|
72
|
+
console.error("Error during server shutdown:", err);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
console.log("Server shut down gracefully.");
|
|
76
|
+
process.exit(0);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
process.on('SIGTERM', shutdown);
|
|
81
|
+
process.on('SIGHUP', shutdown);
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
process .on('unhandledRejection', (reason, p) => {
|
|
85
|
+
console.error(reason, 'Unhandled Rejection at Promise', p);
|
|
86
|
+
})
|
|
87
|
+
.on('uncaughtException', err => {
|
|
88
|
+
console.error(err, 'Uncaught Exception thrown');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"scripts": {
|
|
4
|
+
"prepare": "npx htmx-router",
|
|
5
|
+
"dev": "node ./server.js",
|
|
6
|
+
"build": "run-s build:*",
|
|
7
|
+
"build:router": "npx htmx-router",
|
|
8
|
+
"build:client": "vite build",
|
|
9
|
+
"build:server": "vite build --ssr app/entry.server.ts --outDir dist/server",
|
|
10
|
+
"preview": "cross-env NODE_ENV=production node ./server.js",
|
|
11
|
+
"validate": "npx tsc -noEmit"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"cross-env": "^7.0.3",
|
|
16
|
+
"dotenv": "^16.3.1",
|
|
17
|
+
"express": "^4.21.1",
|
|
18
|
+
"morgan": "^1.10.0",
|
|
19
|
+
"npm-run-all": "^4.1.5",
|
|
20
|
+
"htmx-router": "^1.0.0-alpha.1",
|
|
21
|
+
"react": "^19.0.0",
|
|
22
|
+
"react-dom": "^19.0.0",
|
|
23
|
+
"serve-static": "^1.16.2",
|
|
24
|
+
"tsconfig-paths": "^4.2.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/express": "^5.0.0",
|
|
28
|
+
"@types/node": "^20.4.5",
|
|
29
|
+
"@types/react-dom": "^19.0.2",
|
|
30
|
+
"@types/react": "^19.0.1",
|
|
31
|
+
"@types/serve-static": "^1.15.7",
|
|
32
|
+
"ts-node": "^10.9.1",
|
|
33
|
+
"typed-htmx": "^0.3.1",
|
|
34
|
+
"typescript": "^5.1.6",
|
|
35
|
+
"vite-tsconfig-paths": "^5.1.3",
|
|
36
|
+
"vite": "^6.0.1"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createRequestHandler } from 'htmx-router';
|
|
2
|
+
import { renderToString } from 'react-dom/server';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import morgan from "morgan";
|
|
5
|
+
|
|
6
|
+
const port = process.env.PORT || 5173;
|
|
7
|
+
const app = express();
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const viteDevServer =
|
|
11
|
+
process.env.NODE_ENV === "production"
|
|
12
|
+
? null
|
|
13
|
+
: await import("vite").then((vite) =>
|
|
14
|
+
vite.createServer({
|
|
15
|
+
server: { middlewareMode: true },
|
|
16
|
+
appType: 'custom'
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
app.use(
|
|
21
|
+
viteDevServer
|
|
22
|
+
? viteDevServer.middlewares
|
|
23
|
+
: express.static("./dist/client")
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// logging
|
|
27
|
+
app.use(morgan("tiny"));
|
|
28
|
+
|
|
29
|
+
const build = viteDevServer
|
|
30
|
+
? () => viteDevServer.ssrLoadModule('./app/entry.server.ts')
|
|
31
|
+
: await import('./dist/server/entry.server.js');
|
|
32
|
+
|
|
33
|
+
app.use('*', createRequestHandler.http({
|
|
34
|
+
build, viteDevServer,
|
|
35
|
+
render: (res) => {
|
|
36
|
+
const headers = new Headers();
|
|
37
|
+
headers.set("Content-Type", "text/html; charset=UTF-8");
|
|
38
|
+
|
|
39
|
+
const stream = renderToString(res);
|
|
40
|
+
return new Response(stream, { headers });
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// Start http server
|
|
46
|
+
app.listen(port, () => {
|
|
47
|
+
console.log(`Server started at http://localhost:${port}`)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
// Reload pages on file change
|
|
52
|
+
if (viteDevServer)
|
|
53
|
+
viteDevServer.watcher.on('change', (file) => {
|
|
54
|
+
console.log(`File changed: ${file}`);
|
|
55
|
+
|
|
56
|
+
console.log('Triggering full page reload');
|
|
57
|
+
viteDevServer.ws.send({ type: 'full-reload' });
|
|
58
|
+
});
|
package/global.d.ts
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { GenericContext, RouteContext } from "./router.js";
|
|
2
|
+
import type { ParameterShaper } from "./util/parameters.js";
|
|
3
|
+
import { createRequestHandler } from "./internal/request/index.js";
|
|
4
|
+
export type RenderFunction<T> = (args: T) => Promise<Response | JSX.Element | null>;
|
|
5
|
+
export type CatchFunction<T> = (args: T, err: unknown) => Promise<Response | JSX.Element>;
|
|
6
|
+
export type RouteModule<T extends ParameterShaper> = {
|
|
7
|
+
parameters?: T;
|
|
8
|
+
loader?: RenderFunction<RouteContext<T>>;
|
|
9
|
+
action?: RenderFunction<RouteContext<T>>;
|
|
10
|
+
error?: CatchFunction<RouteContext<T>>;
|
|
11
|
+
route?: (params: Record<string, string>) => string;
|
|
12
|
+
};
|
|
13
|
+
export type ClientIslandManifest<T> = {
|
|
14
|
+
[K in keyof T]: ClientIsland<T[K]>;
|
|
15
|
+
};
|
|
16
|
+
type ClientIsland<T> = T extends (props: infer P) => JSX.Element ? (props: P & {
|
|
17
|
+
children?: JSX.Element;
|
|
18
|
+
}) => JSX.Element : T;
|
|
19
|
+
export { createRequestHandler, GenericContext, RouteContext };
|
package/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
const DEFAULT = {
|
|
4
|
+
framework: "generic"
|
|
5
|
+
};
|
|
6
|
+
export async function ReadConfig() {
|
|
7
|
+
const path = process.argv[2] || "./htmx.config.json";
|
|
8
|
+
if (!existsSync(path))
|
|
9
|
+
return DEFAULT;
|
|
10
|
+
return JSON.parse(await readFile(path, "utf-8"));
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
import { readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { CompileManifest } from "../compile/manifest.js";
|
|
5
|
+
import { ReadConfig } from "../cli/config.js";
|
|
6
|
+
const config = await ReadConfig();
|
|
7
|
+
if (config.client) {
|
|
8
|
+
console.info("Building client island manifest");
|
|
9
|
+
const source = await readFile(config.client.source, "utf8");
|
|
10
|
+
await writeFile(config.client.output.server, CompileManifest(config.framework, source, true));
|
|
11
|
+
await writeFile(config.client.output.client, CompileManifest(config.framework, source, false));
|
|
12
|
+
}
|
|
13
|
+
if (config.component) {
|
|
14
|
+
console.info("Building components");
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function GetClientEntryURL(): Promise<string | undefined>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ServerOnlyWarning } from "./util.js";
|
|
2
|
+
ServerOnlyWarning("client-url");
|
|
1
3
|
import { readFile } from "fs/promises";
|
|
2
4
|
export async function GetClientEntryURL() {
|
|
3
5
|
if (process.env.NODE_ENV !== "production")
|
|
@@ -7,6 +9,6 @@ export async function GetClientEntryURL() {
|
|
|
7
9
|
const def = config[key];
|
|
8
10
|
if (!def.isEntry)
|
|
9
11
|
continue;
|
|
10
|
-
return def.file;
|
|
12
|
+
return "/" + def.file;
|
|
11
13
|
}
|
|
12
14
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function CompileManifest(adapter: string, source: string, ssr: boolean): string;
|