htmx-router 0.0.1

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/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/bin/cli.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const router_1 = require("./router");
7
+ const cwd = process.argv[2] || "./";
8
+ const rootMatcher = new RegExp(/^root\.(j|t)sx?$/);
9
+ const root = (0, fs_1.readdirSync)(cwd)
10
+ .filter(x => rootMatcher.test(x))[0];
11
+ if (!root) {
12
+ console.log(`Missing root.jsx/tsx`);
13
+ process.exit(1);
14
+ }
15
+ function readDirRecursively(dir) {
16
+ const files = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
17
+ let filePaths = [];
18
+ for (const file of files) {
19
+ if (file.isDirectory()) {
20
+ filePaths = [...filePaths, ...readDirRecursively((0, path_1.join)(dir, file.name))];
21
+ }
22
+ else {
23
+ filePaths.push((0, path_1.join)(dir, file.name));
24
+ }
25
+ }
26
+ return filePaths;
27
+ }
28
+ const DIR = './routes';
29
+ const files = readDirRecursively(`${cwd}/routes`)
30
+ .filter(x => (0, router_1.IsAllowedExt)((0, path_1.extname)(x).slice(1)))
31
+ .map(x => (0, path_1.relative)(cwd, x.slice(0, x.lastIndexOf("."))).replace(/\\/g, "/"))
32
+ .sort();
33
+ let script = `import { RouteTree } from "htmx-router";\n`;
34
+ for (let i = 0; i < files.length; i++) {
35
+ const file = files[i];
36
+ script += `import * as Route${i} from "./${file}";\n`;
37
+ }
38
+ script += `import * as RootRoute from "./root";\n`;
39
+ script += `\nexport const Router = new RouteTree;\n`;
40
+ for (let i = 0; i < files.length; i++) {
41
+ const file = files[i];
42
+ script += `Router.ingest("${file.slice(DIR.length - 1)}", Route${i}, []);\n`;
43
+ }
44
+ script += `Router.assignRoot(RootRoute);\n`;
45
+ (0, fs_1.writeFileSync)(`${cwd}/router.ts`, script);
46
+ console.log(`Build with routes;\n` + files.map(x => ` - ${x}`).join("\n"));
@@ -0,0 +1,3 @@
1
+ export declare function StyleCSS(props: {
2
+ [key: string]: string | number;
3
+ }): string;
package/bin/helper.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StyleCSS = void 0;
4
+ function StyleCSS(props) {
5
+ let out = "";
6
+ for (const key in props) {
7
+ const safeKey = key.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
8
+ const safeVal = props[key].toString().replace(/"/g, "\\\"");
9
+ out += `${safeKey}: ${safeVal};`;
10
+ }
11
+ return out;
12
+ }
13
+ exports.StyleCSS = StyleCSS;
package/bin/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { RouteTree } from "./router";
2
+ import { ErrorResponse, Redirect, Outlet, Override, RenderArgs } from "./shared";
3
+ import { StyleCSS } from "./helper";
4
+ export { RouteTree, ErrorResponse, Redirect, Override, RenderArgs, Outlet, StyleCSS };
package/bin/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StyleCSS = exports.RenderArgs = exports.Override = exports.Redirect = exports.ErrorResponse = exports.RouteTree = void 0;
4
+ const router_1 = require("./router");
5
+ Object.defineProperty(exports, "RouteTree", { enumerable: true, get: function () { return router_1.RouteTree; } });
6
+ const shared_1 = require("./shared");
7
+ Object.defineProperty(exports, "ErrorResponse", { enumerable: true, get: function () { return shared_1.ErrorResponse; } });
8
+ Object.defineProperty(exports, "Redirect", { enumerable: true, get: function () { return shared_1.Redirect; } });
9
+ Object.defineProperty(exports, "Override", { enumerable: true, get: function () { return shared_1.Override; } });
10
+ Object.defineProperty(exports, "RenderArgs", { enumerable: true, get: function () { return shared_1.RenderArgs; } });
11
+ const helper_1 = require("./helper");
12
+ Object.defineProperty(exports, "StyleCSS", { enumerable: true, get: function () { return helper_1.StyleCSS; } });
@@ -0,0 +1,23 @@
1
+ /// <reference types="node" />
2
+ import type http from "node:http";
3
+ import { Outlet, RenderArgs, RouteModule } from "./shared";
4
+ export declare function IsAllowedExt(ext: string): boolean;
5
+ declare class RouteLeaf {
6
+ module: RouteModule;
7
+ mask: boolean[];
8
+ constructor(module: RouteModule, mask: boolean[]);
9
+ makeOutlet(args: RenderArgs, outlet: Outlet): Outlet;
10
+ }
11
+ export declare class RouteTree {
12
+ nested: Map<string, RouteTree>;
13
+ wild: RouteTree | null;
14
+ wildCard: string;
15
+ default: RouteLeaf | null;
16
+ route: RouteLeaf | null;
17
+ constructor();
18
+ assignRoot(module: RouteModule): void;
19
+ ingest(path: string | string[], module: RouteModule, override: boolean[]): void;
20
+ render(req: http.IncomingMessage, res: http.ServerResponse, url: URL): Promise<string> | "";
21
+ private _recursiveRender;
22
+ }
23
+ export {};
package/bin/router.js ADDED
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RouteTree = exports.IsAllowedExt = void 0;
4
+ const shared_1 = require("./shared");
5
+ function IsAllowedExt(ext) {
6
+ // js, jsx, tsx, ts
7
+ if (ext[1] !== "s")
8
+ return false;
9
+ if (ext[0] !== "j" && ext[0] !== "t")
10
+ return false;
11
+ if (ext.length == 2)
12
+ return true;
13
+ if (ext.length != 3)
14
+ return false;
15
+ if (ext[2] !== "x")
16
+ return false;
17
+ return true;
18
+ }
19
+ exports.IsAllowedExt = IsAllowedExt;
20
+ async function blankOutlet() {
21
+ return "";
22
+ }
23
+ class RouteLeaf {
24
+ constructor(module, mask) {
25
+ this.module = module;
26
+ this.mask = mask;
27
+ }
28
+ makeOutlet(args, outlet) {
29
+ const renderer = this.module.Render || blankOutlet;
30
+ const catcher = this.module.CatchError;
31
+ return async () => {
32
+ try {
33
+ return await renderer(args, outlet);
34
+ }
35
+ catch (e) {
36
+ if (e instanceof shared_1.Redirect || e instanceof shared_1.Override)
37
+ throw e;
38
+ const err = (e instanceof shared_1.ErrorResponse) ? e :
39
+ new shared_1.ErrorResponse(500, "Runtime Error", e);
40
+ if (catcher)
41
+ return await catcher(args, err);
42
+ throw err;
43
+ }
44
+ };
45
+ }
46
+ }
47
+ class RouteTree {
48
+ constructor() {
49
+ this.nested = new Map();
50
+ this.wildCard = "";
51
+ this.wild = null;
52
+ this.default = null;
53
+ this.route = null;
54
+ }
55
+ assignRoot(module) {
56
+ if (!module.Render)
57
+ throw new Error(`Root route is missing Render()`);
58
+ if (!module.CatchError)
59
+ throw new Error(`Root route is missing CatchError()`);
60
+ this.route = new RouteLeaf(module, []);
61
+ }
62
+ ingest(path, module, override) {
63
+ if (!Array.isArray(path)) {
64
+ path = path.split(/[\./\\]/g);
65
+ }
66
+ if (path.length === 0) {
67
+ this.route = new RouteLeaf(module, override);
68
+ return;
69
+ }
70
+ if (path.length === 1 && path[0] === "_index") {
71
+ this.default = new RouteLeaf(module, override);
72
+ return;
73
+ }
74
+ if (path[0].endsWith("_")) {
75
+ path[0] = path[0].slice(0, -1);
76
+ override.push(true);
77
+ }
78
+ else {
79
+ override.push(false);
80
+ }
81
+ if (path[0][0] === "$") {
82
+ const wildCard = path[0].slice(1);
83
+ // Check wildcard isn't being changed
84
+ if (!this.wild) {
85
+ this.wildCard = wildCard;
86
+ this.wild = new RouteTree();
87
+ }
88
+ else if (wildCard !== this.wildCard) {
89
+ throw new Error(`Redefinition of wild card ${this.wildCard} to ${wildCard}`);
90
+ }
91
+ path.splice(0, 1);
92
+ this.wild.ingest(path, module, override);
93
+ return;
94
+ }
95
+ let next = this.nested.get(path[0]);
96
+ if (!next) {
97
+ next = new RouteTree();
98
+ this.nested.set(path[0], next);
99
+ }
100
+ path.splice(0, 1);
101
+ next.ingest(path, module, override);
102
+ }
103
+ render(req, res, url) {
104
+ const args = new shared_1.RenderArgs(req, res, url);
105
+ if (!this.default || !this.default.module.Render) {
106
+ return "";
107
+ }
108
+ const frags = url.pathname.split('/').slice(1);
109
+ if (frags.length === 1 && frags[0] === "") {
110
+ frags.splice(0, 1);
111
+ }
112
+ return this._recursiveRender(args, frags).outlet();
113
+ }
114
+ _recursiveRender(args, frags) {
115
+ var _a;
116
+ let out = {
117
+ outlet: blankOutlet,
118
+ mask: [],
119
+ };
120
+ if (frags.length == 0) {
121
+ if (!this.default) {
122
+ out.outlet = () => {
123
+ throw new shared_1.ErrorResponse(404, "Resource Not Found", `Unable to find ${args.url.pathname}`);
124
+ };
125
+ }
126
+ else if ((_a = this.default) === null || _a === void 0 ? void 0 : _a.module.Render) {
127
+ out.outlet = this.default.makeOutlet(args, out.outlet);
128
+ out.mask = [...this.default.mask];
129
+ }
130
+ }
131
+ else {
132
+ const segment = frags.splice(0, 1)[0];
133
+ const subRoute = this.nested.get(segment);
134
+ if (subRoute) {
135
+ out = subRoute._recursiveRender(args, frags);
136
+ }
137
+ else if (this.wild) {
138
+ args.params[this.wildCard] = segment;
139
+ out = this.wild._recursiveRender(args, frags);
140
+ }
141
+ else {
142
+ out.outlet = () => {
143
+ throw new shared_1.ErrorResponse(404, "Resource Not Found", `Unable to find ${args.url.pathname}`);
144
+ };
145
+ }
146
+ }
147
+ // Is this route masked out?
148
+ const ignored = out.mask.splice(0, 1)[0] === true;
149
+ if (!ignored && this.route) {
150
+ out.outlet = this.route.makeOutlet(args, out.outlet);
151
+ }
152
+ return out;
153
+ }
154
+ }
155
+ exports.RouteTree = RouteTree;
@@ -0,0 +1,40 @@
1
+ /// <reference types="node" />
2
+ import type http from "node:http";
3
+ export type Outlet = () => Promise<string>;
4
+ export type RenderFunction = (args: RenderArgs, Outlet: Outlet) => Promise<string>;
5
+ export type CatchFunction = (args: RenderArgs, err: ErrorResponse) => Promise<string>;
6
+ export type RouteModule = {
7
+ Render?: RenderFunction;
8
+ CatchError?: CatchFunction;
9
+ };
10
+ export declare class ErrorResponse {
11
+ code: number;
12
+ status: string;
13
+ data: any;
14
+ constructor(statusCode: number, statusMessage: string, data?: any);
15
+ }
16
+ export declare class Redirect {
17
+ location: string;
18
+ constructor(location: string);
19
+ run(res: http.ServerResponse): http.ServerResponse<http.IncomingMessage>;
20
+ }
21
+ export declare class Override {
22
+ data: BufferSource;
23
+ constructor(data: BufferSource);
24
+ }
25
+ type MetaHTML = {
26
+ [key: string]: string;
27
+ };
28
+ export declare class RenderArgs {
29
+ req: http.IncomingMessage;
30
+ res: http.ServerResponse;
31
+ params: MetaHTML;
32
+ url: URL;
33
+ links: MetaHTML[];
34
+ meta: MetaHTML[];
35
+ constructor(req: http.IncomingMessage, res: http.ServerResponse, url: URL);
36
+ addLinks(links: MetaHTML[], override?: boolean): void;
37
+ addMeta(links: MetaHTML[], override?: boolean): void;
38
+ renderHeadHTML(): string;
39
+ }
40
+ export {};
package/bin/shared.js ADDED
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RenderArgs = exports.Override = exports.Redirect = exports.ErrorResponse = void 0;
4
+ class ErrorResponse {
5
+ constructor(statusCode, statusMessage, data) {
6
+ this.code = statusCode;
7
+ this.status = statusMessage;
8
+ this.data = data || "";
9
+ }
10
+ }
11
+ exports.ErrorResponse = ErrorResponse;
12
+ class Redirect {
13
+ constructor(location) {
14
+ this.location = location;
15
+ }
16
+ run(res) {
17
+ res.statusCode = 302;
18
+ res.setHeader('Location', this.location);
19
+ return res.end();
20
+ }
21
+ }
22
+ exports.Redirect = Redirect;
23
+ class Override {
24
+ constructor(data) {
25
+ this.data = data;
26
+ }
27
+ }
28
+ exports.Override = Override;
29
+ const attrRegex = /[A-z]+/;
30
+ function ValidateMetaHTML(val) {
31
+ for (const key in val) {
32
+ if (!attrRegex.test(key))
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ function ValidateMetaHTMLs(val) {
38
+ for (const meta of val) {
39
+ if (!ValidateMetaHTML(meta))
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+ class RenderArgs {
45
+ constructor(req, res, url) {
46
+ this.req = req;
47
+ this.res = res;
48
+ this.url = url;
49
+ this.params = {};
50
+ this.links = [];
51
+ this.meta = [];
52
+ }
53
+ addLinks(links, override = false) {
54
+ if (!ValidateMetaHTMLs(links))
55
+ throw new Error(`Provided links have invalid attribute`);
56
+ if (override) {
57
+ this.links = links;
58
+ }
59
+ else {
60
+ this.links.push(...links);
61
+ }
62
+ }
63
+ addMeta(links, override = false) {
64
+ if (!ValidateMetaHTMLs(links))
65
+ throw new Error(`Provided links have invalid attribute`);
66
+ if (override) {
67
+ this.meta = links;
68
+ }
69
+ else {
70
+ this.meta.push(...links);
71
+ }
72
+ }
73
+ renderHeadHTML() {
74
+ let out = "";
75
+ for (const elm of this.links) {
76
+ out += "<link";
77
+ for (const attr in elm) {
78
+ out += ` ${attr}="${elm[attr].replace(/"/g, "\\\"")}"`;
79
+ }
80
+ out += "></link>";
81
+ }
82
+ for (const elm of this.meta) {
83
+ out += "<meta";
84
+ for (const attr in elm) {
85
+ out += ` ${attr}="${elm[attr].replace(/"/g, "\\\"")}"`;
86
+ }
87
+ out += "></meta>";
88
+ }
89
+ return out;
90
+ }
91
+ }
92
+ exports.RenderArgs = RenderArgs;
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "htmx-router",
3
+ "version": "0.0.1",
4
+ "description": "A remix.js style file path router for htmX websites",
5
+ "main": "./bin/index.js",
6
+ "scripts": {
7
+ "build": "tsc"
8
+ },
9
+ "bin": {
10
+ "htmx-router": "bin/cli.js"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/AjaniBilby/htmx-router.git"
15
+ },
16
+ "author": "Ajani Bilby",
17
+ "license": "MIT",
18
+ "bugs": {
19
+ "url": "https://github.com/AjaniBilby/htmx-router/issues"
20
+ },
21
+ "homepage": "https://github.com/AjaniBilby/htmx-router#readme",
22
+ "devDependencies": {
23
+ "@types/node": "^20.4.5",
24
+ "ts-node": "^10.9.1",
25
+ "typescript": "^5.1.6"
26
+ }
27
+ }
package/readme.md ADDED
@@ -0,0 +1,6 @@
1
+ # htmX Router
2
+
3
+ > A remix.js style file path router for htmX websites
4
+
5
+
6
+ *Documentation coming soon*