htmx-router 1.0.0-alpha.5 → 1.0.0-pre1
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/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/package.json +10 -8
- package/readme.md +17 -212
- package/bin/cli/config.d.ts +0 -10
- package/bin/cli/config.js +0 -4
- package/bin/cli/index.d.ts +0 -2
- package/bin/cli/index.js +0 -66
- package/bin/client/entry.d.ts +0 -1
- package/bin/client/entry.js +0 -12
- package/bin/client/index.d.ts +0 -7
- package/bin/client/index.js +0 -132
- package/bin/client/mount.d.ts +0 -2
- package/bin/client/mount.js +0 -116
- package/bin/client/watch.d.ts +0 -1
- package/bin/client/watch.js +0 -11
- package/bin/helper.d.ts +0 -2
- package/bin/helper.js +0 -34
- package/bin/index.d.ts +0 -11
- package/bin/index.js +0 -18
- package/bin/request/http.d.ts +0 -10
- package/bin/request/http.js +0 -41
- package/bin/request/index.d.ts +0 -16
- package/bin/request/index.js +0 -6
- package/bin/request/native.d.ts +0 -9
- package/bin/request/native.js +0 -46
- package/bin/response.d.ts +0 -9
- package/bin/response.js +0 -46
- package/bin/router.d.ts +0 -49
- package/bin/router.js +0 -217
- package/bin/types.d.ts +0 -10
- package/bin/types.js +0 -1
- package/bin/util/cookies.d.ts +0 -25
- package/bin/util/cookies.js +0 -60
- package/bin/util/css.d.ts +0 -13
- package/bin/util/css.js +0 -55
- package/bin/util/dynamic.d.ts +0 -8
- package/bin/util/dynamic.js +0 -40
- package/bin/util/endpoint.d.ts +0 -13
- package/bin/util/endpoint.js +0 -32
- package/bin/util/event-source.d.ts +0 -16
- package/bin/util/event-source.js +0 -85
- package/bin/util/hash.d.ts +0 -4
- package/bin/util/hash.js +0 -10
- package/bin/util/index.d.ts +0 -1
- package/bin/util/index.js +0 -7
- package/bin/util/parameters.d.ts +0 -10
- package/bin/util/parameters.js +0 -17
- package/bin/util/path-builder.d.ts +0 -1
- package/bin/util/path-builder.js +0 -43
- package/bin/util/response.d.ts +0 -11
- package/bin/util/response.js +0 -46
- package/bin/util/shell.d.ts +0 -120
- package/bin/util/shell.js +0 -251
|
@@ -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/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "htmx-router",
|
|
3
|
-
"version": "1.0.0-
|
|
4
|
-
"description": "A
|
|
5
|
-
"keywords": [
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-pre1",
|
|
4
|
+
"description": "A lightweight SSR framework with server+client islands",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"htmx", "router", "client islands", "ssr", "vite"
|
|
7
|
+
],
|
|
8
|
+
"main": "./index.js",
|
|
7
9
|
"type": "module",
|
|
8
10
|
"scripts": {
|
|
9
|
-
"build": "tsc
|
|
11
|
+
"build": "tsc"
|
|
10
12
|
},
|
|
11
13
|
"bin": {
|
|
12
|
-
"htmx-router": "
|
|
14
|
+
"htmx-router": "cli/index.js"
|
|
13
15
|
},
|
|
14
16
|
"repository": {
|
|
15
17
|
"type": "git",
|
|
@@ -23,12 +25,12 @@
|
|
|
23
25
|
"homepage": "https://github.com/AjaniBilby/htmx-router#readme",
|
|
24
26
|
"dependencies": {
|
|
25
27
|
"es-module-lexer": "^1.5.4",
|
|
26
|
-
"vite": "^6.0.
|
|
28
|
+
"vite": "^6.0.6"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"@types/node": "^20.4.5",
|
|
32
|
+
"chalk": "^5.4.1",
|
|
30
33
|
"ts-node": "^10.9.1",
|
|
31
|
-
"tsc-alias": "^1.8.10",
|
|
32
34
|
"typescript": "^5.1.6"
|
|
33
35
|
}
|
|
34
36
|
}
|
package/readme.md
CHANGED
|
@@ -1,217 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# HTMX Router
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A lightweight file based router built on vite+htmx for SSR generation of full pages and html partials
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- [Setup](#setup)
|
|
7
|
-
- [Routing](#routing)
|
|
8
|
-
- [Route Module](#route-module)
|
|
9
|
-
- [Nested Route Rendering](#nested-route-rendering)
|
|
10
|
-
- [JSX Rendering](#jsx-rendering)
|
|
11
|
-
- [Route Contexts](#route-contexts)
|
|
12
|
-
- [Params](#params)
|
|
13
|
-
- [Cookies](#cookies)
|
|
14
|
-
- [Headers](#headers)
|
|
15
|
-
- [Request](#request)
|
|
16
|
-
- [URL](#url)
|
|
17
|
-
- [Style Sheets](#style-sheets)
|
|
18
|
-
- [Route-less Endpoint](#route-less-endpoint)
|
|
19
|
-
- [Islands](#islands)
|
|
20
|
-
- [Dynamic](#dynamic)
|
|
21
|
-
- [Client](#client)
|
|
5
|
+
Features:
|
|
22
6
|
|
|
7
|
+
- BYO jsx templating
|
|
8
|
+
- File base routing
|
|
9
|
+
- Typesafe url path parameters
|
|
10
|
+
- Dynamic route fallthrough
|
|
11
|
+
- Server + Client Islands
|
|
12
|
+
- Route-less points
|
|
13
|
+
- CSS sheet generation
|
|
14
|
+
- [html](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta), [opengraph](https://ogp.me/), and [json+ld](https://json-ld.org/) metadata generation
|
|
15
|
+
- Bundle splitting for filtering out server code from the client
|
|
16
|
+
- Server-Side EventSource creation for SSE event dispatch
|
|
23
17
|
|
|
24
|
-
##
|
|
18
|
+
## Documentation
|
|
19
|
+
See https://htmx-router.ajanibilby.com/
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```json
|
|
29
|
-
{
|
|
30
|
-
"router": {
|
|
31
|
-
"folder": "./source/routes",
|
|
32
|
-
"output": "./source/router.tsx"
|
|
33
|
-
},
|
|
34
|
-
"client": {
|
|
35
|
-
"adapter": "react",
|
|
36
|
-
"source": "./source/client.tsx"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Once you have done this, run `npx htmx-router` to generate the artifacts to start development.
|
|
42
|
-
We recommand you copy the setup from `examples/react` for your `server.js`, `entry.server.ts`, and `entry.client.ts`.
|
|
43
|
-
|
|
44
|
-
Don't forget that in all rendered routes you must include the `<RouterHeader/>` component in your head for hydration and `StyleClass`s to apply affectively.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
## Routing
|
|
48
|
-
|
|
49
|
-
Routing applies in a depth first order, where it will match in order:
|
|
50
|
-
1. The `_index`
|
|
51
|
-
2. Static sub-routes
|
|
52
|
-
3. Url path-param wildcards
|
|
53
|
-
4. Catchall slug
|
|
54
|
-
|
|
55
|
-
This allows for easy overriding and fallback behaviour. For instance with the routes.
|
|
56
|
-
```
|
|
57
|
-
/user/$id.tsx
|
|
58
|
-
/user/me.tsx
|
|
59
|
-
/user/$.tsx
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
It will match on the `/user/me` route if applicable, and otherwise will fallback to attempt to match on `/user/$id`, and if the wildcard route fails, it will try the generic slug route `/user/$.tsx`.
|
|
63
|
-
|
|
64
|
-
If a route returns `null` the router will continue the depth first search, allowing for dynamic flow through of the routes.
|
|
65
|
-
|
|
66
|
-
### Route Module
|
|
67
|
-
|
|
68
|
-
A route module must define a `parameters` export, which defines how the url path params should be parsed when attempting to match the route.
|
|
69
|
-
You can use any function which takes a string, and returns something as the parser. You can also simply use JS-Builtin functions for this, and there is a special case with the `Number` function so it will reject on `NaN` values.
|
|
70
|
-
```js
|
|
71
|
-
export const parameters = { id: Number };
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
A route can additionally define a loader, which is called on `GET` and `HEAD` requests
|
|
75
|
-
```ts
|
|
76
|
-
export async function loader({}: RouteContext<typeof parameters>);
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
With the `action` function being called for all other methods
|
|
80
|
-
```ts
|
|
81
|
-
export async function action({}: RouteContext<typeof parameters>);
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
If any value is thrown by the parameter parsing, or the render functions (`loader`/`action`) it will boil up, attempting first to call an error function is supplied in the route, and otherwise boiling up to the nearest slug route's `error` function.
|
|
85
|
-
```ts
|
|
86
|
-
export async function error(ctx: GenericContext, error: unknown);
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### Nested Route Rendering
|
|
90
|
-
|
|
91
|
-
The router will not do nested layouts, if that behaviour is required we recommend using the slug-shell pattern.
|
|
92
|
-
Where you define a slug route, and export a `shell` function which takes the `JSX` rendered result from the sub route, and renders the upper route around it.
|
|
93
|
-
|
|
94
|
-
This allows flexibility at runtime on how nested route rendering behaves, and can also allow you to ensure you are not reloading data from the db which is already loaded by a sub-route based on how you parse up data through your slug shells.
|
|
95
|
-
|
|
96
|
-
We recommend you look at [Predictable Bot](https://github.com/AjaniBilby/predictable) as an example of this pattern performed simply.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
### JSX Rendering
|
|
100
|
-
|
|
101
|
-
htmx-router is jsx templating agnostic for SSR, instead only requiring a definition provided when creating your request handler, allowing you to BYO JSX templating.
|
|
102
|
-
```js
|
|
103
|
-
// @kitajs/html
|
|
104
|
-
app.use('*', createRequestHandler.http({
|
|
105
|
-
build,
|
|
106
|
-
viteDevServer,
|
|
107
|
-
render: (res) => {
|
|
108
|
-
const headers = new Headers();
|
|
109
|
-
headers.set("Content-Type", "text/html; charset=UTF-8");
|
|
110
|
-
return new Response(String(res), { headers });
|
|
111
|
-
}
|
|
112
|
-
}));
|
|
113
|
-
|
|
114
|
-
// React
|
|
115
|
-
app.use('*', createRequestHandler.http({
|
|
116
|
-
build,
|
|
117
|
-
viteDevServer,
|
|
118
|
-
render: (res) => {
|
|
119
|
-
const headers = new Headers();
|
|
120
|
-
headers.set("Content-Type", "text/html; charset=UTF-8");
|
|
121
|
-
return new Response(renderToString(res), { headers });
|
|
122
|
-
}
|
|
123
|
-
}));
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
## Route Contexts
|
|
127
|
-
|
|
128
|
-
There are two kinds of route context, the `RouteContext<T>` which is the resolved route with parameterization, and the `GenericContext` which is used by error functions, and dynamic loads.
|
|
129
|
-
|
|
130
|
-
### Params
|
|
131
|
-
|
|
132
|
-
In the `GenericContext` this will simply be an object with string key value pairs for the parameters, and only the `RouteContext<T>` for `loader` and `action` will have the parameters pre-parsed by your `parameters` definition.
|
|
133
|
-
|
|
134
|
-
### Cookies
|
|
135
|
-
|
|
136
|
-
The `RouteContext` and `GenericContext`s both provide a `cookie` object, with the cookie's pre-parsed from the request headers.
|
|
137
|
-
It also has a built in `set(name, value, options)` function which will add the appropriate headers to the response for the cookie changes.
|
|
138
|
-
|
|
139
|
-
### Headers
|
|
140
|
-
|
|
141
|
-
This is a header object useful for adding response headers when you haven't fully finished generating your response yet.
|
|
142
|
-
These headers will merge with the response object created by the provided `render` function, with response headers overriding any conflicting `ctx.headers` values.
|
|
143
|
-
|
|
144
|
-
### Request
|
|
145
|
-
|
|
146
|
-
This is the original request object, including request headers.
|
|
147
|
-
|
|
148
|
-
### URL
|
|
149
|
-
|
|
150
|
-
The parsed `URL` object of the incoming request.
|
|
151
|
-
|
|
152
|
-
## Style Sheets
|
|
153
|
-
|
|
154
|
-
htmx-router includes a `StyleClass` object, which can be used to define CSS classes without needing a unique name.
|
|
155
|
-
StyleClasses should only be defined at the top level of a file, and not created within a function, or dynamically during runtime.
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
const myClass = new StyleClass(`myClass`, `
|
|
159
|
-
.this:hover {
|
|
160
|
-
background-color: red;
|
|
161
|
-
}
|
|
162
|
-
`).name;
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## Route-less Endpoint
|
|
166
|
-
|
|
167
|
-
This should be defined at the top level of your file, these endpoints can optionally be given an name which will help for debugging network requests, but they do not need to be unique.
|
|
168
|
-
```ts
|
|
169
|
-
const endpoint_url = new Endpoint((ctx: GenericContext) => {
|
|
170
|
-
return new Response("Hello World");
|
|
171
|
-
}, "hello-world").url;
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
## Islands
|
|
175
|
-
|
|
176
|
-
> Tip: Don't forget to wrap your islands in a hx-preserve to prevent losing state. And use `display: contents;` to make that wrapping div transparent for grid and other layout features.
|
|
177
|
-
|
|
178
|
-
### Dynamic
|
|
179
|
-
|
|
180
|
-
A dynamic component takes params which will be converted into the props of the loader function, these props may only be string key string value pairs as they are encoded the the query string to allow for browser side caching.
|
|
181
|
-
|
|
182
|
-
The body of a dynamic component is the pre-rendered infill that will display while the client is loading the dynamic content.
|
|
183
|
-
|
|
184
|
-
```tsx
|
|
185
|
-
async function MyProfile(params: {}, ctx: GenericContext): Promise<JSX.Element> {
|
|
186
|
-
ctx.headers.set('Cache-Control', "private, max-age=120");
|
|
187
|
-
const userID = ctx.cookie.get('userID');
|
|
188
|
-
if (!userID) return <></>;
|
|
189
|
-
|
|
190
|
-
const user = await GetUser(userID);
|
|
191
|
-
if (!user) return <></>;
|
|
192
|
-
|
|
193
|
-
return <a href={`/user/${userID}`}>
|
|
194
|
-
<div safe>{user.name}</div>
|
|
195
|
-
</a>
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export async function loader({ params }: RouteContext<typeof parameters>) {
|
|
199
|
-
return <Dynamic params={{}} loader={MyProfile}>
|
|
200
|
-
put your ssr pre-rendered skeleton here
|
|
201
|
-
</Dynamic>
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### Client
|
|
206
|
-
|
|
207
|
-
Import all of the components you want to be able to use on the client side into your `client.tsx`, if you are running a dev server this file will automatically generate the clientized version, otherwise use the `npx htmx-router` command to regenerate these artifacts.
|
|
208
|
-
|
|
209
|
-
Once a component has been clientized you can import it as use it like normal, however the body is now overwritten to it will render immediately on the server, and then all props will parsed to the client for it to be rendered properly in the browser.
|
|
210
|
-
|
|
211
|
-
```tsx
|
|
212
|
-
<Client.Counter>
|
|
213
|
-
<button>No yet hydrated...</button> {/* this will be overwritten in the browser once hydrated */}
|
|
214
|
-
</Client.Counter>
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
It is very important that you ensure your `Client` component has a single child element, if there are multiple child components the browser will only mount to the last child causing artifacting.
|
|
21
|
+
## API
|
|
22
|
+
See https://htmx-router.ajanibilby.com/api
|
package/bin/cli/config.d.ts
DELETED
package/bin/cli/config.js
DELETED
package/bin/cli/index.d.ts
DELETED
package/bin/cli/index.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
import { writeFile } from "fs/promises";
|
|
4
|
-
import { relative } from "path";
|
|
5
|
-
import { GenerateClient } from "../client/index.js";
|
|
6
|
-
import { ReadConfig } from "../cli/config.js";
|
|
7
|
-
const config = await ReadConfig();
|
|
8
|
-
console.info("Building router");
|
|
9
|
-
const routes = relative(config.router.output, config.router.folder).replaceAll("\\", "/").slice(1);
|
|
10
|
-
await writeFile(config.router.output, `/*------------------------------------------
|
|
11
|
-
* Generated by htmx-router *
|
|
12
|
-
* Warn: Any changes will be overwritten *
|
|
13
|
-
-------------------------------------------*/
|
|
14
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
15
|
-
|
|
16
|
-
import { GenericContext, RouteTree } from "htmx-router/bin/router";
|
|
17
|
-
import { GetClientEntryURL } from 'htmx-router/bin/client/entry';
|
|
18
|
-
import { DynamicReference } from "htmx-router/bin/util/dynamic";
|
|
19
|
-
import { GetMountUrl } from 'htmx-router/bin/client/mount';
|
|
20
|
-
import { GetSheetUrl } from 'htmx-router/bin/util/css';
|
|
21
|
-
import { RouteModule } from "htmx-router";
|
|
22
|
-
import { resolve } from "path";
|
|
23
|
-
|
|
24
|
-
(globalThis as any).HTMX_ROUTER_ROOT = resolve('${config.router.folder.replaceAll("\\", "/")}');
|
|
25
|
-
const modules = import.meta.glob('${routes}/**/*.{ts,tsx}', { eager: true });
|
|
26
|
-
|
|
27
|
-
export const tree = new RouteTree();
|
|
28
|
-
for (const path in modules) {
|
|
29
|
-
const tail = path.lastIndexOf(".");
|
|
30
|
-
const url = path.slice(${routes.length + 1}, tail);
|
|
31
|
-
tree.ingest(url, modules[path] as RouteModule<any>);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function Dynamic<T extends Record<string, string>>(props: {
|
|
35
|
-
params?: T,
|
|
36
|
-
loader: (ctx: GenericContext, params?: T) => Promise<JSX.Element>
|
|
37
|
-
children?: JSX.Element
|
|
38
|
-
}): JSX.Element {
|
|
39
|
-
return <div
|
|
40
|
-
hx-get={DynamicReference(props.loader, props.params)}
|
|
41
|
-
hx-trigger="load"
|
|
42
|
-
hx-swap="outerHTML transition:true"
|
|
43
|
-
style={{ display: "contents" }}
|
|
44
|
-
>{props.children ? props.children : ""}</div>
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
let headCache: JSX.Element | null = null;
|
|
48
|
-
const isProduction = process.env.NODE_ENV === "production";
|
|
49
|
-
const clientEntry = await GetClientEntryURL();
|
|
50
|
-
export function Scripts() {
|
|
51
|
-
if (headCache) return headCache;
|
|
52
|
-
|
|
53
|
-
const res = <>
|
|
54
|
-
<link href={GetSheetUrl()} rel="stylesheet"></link>
|
|
55
|
-
{ isProduction ? "" : <script type="module" src="/@vite/client"></script> }
|
|
56
|
-
<script type="module" src={clientEntry}></script>
|
|
57
|
-
<script src={GetMountUrl()}></script>
|
|
58
|
-
</>;
|
|
59
|
-
|
|
60
|
-
if (isProduction) headCache = res;
|
|
61
|
-
return res;
|
|
62
|
-
}`);
|
|
63
|
-
if (config.client) {
|
|
64
|
-
console.info("Building client islands");
|
|
65
|
-
await GenerateClient(config.client, true);
|
|
66
|
-
}
|
package/bin/client/entry.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function GetClientEntryURL(): Promise<any>;
|
package/bin/client/entry.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
2
|
-
export async function GetClientEntryURL() {
|
|
3
|
-
if (process.env.NODE_ENV !== "production")
|
|
4
|
-
return "/app/entry.client.ts";
|
|
5
|
-
const config = JSON.parse(await readFile("./dist/client/.vite/manifest.json", "utf8"));
|
|
6
|
-
for (const key in config) {
|
|
7
|
-
const def = config[key];
|
|
8
|
-
if (!def.isEntry)
|
|
9
|
-
continue;
|
|
10
|
-
return def.file;
|
|
11
|
-
}
|
|
12
|
-
}
|