htmx-router 1.0.0-alpha.5 → 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.
Files changed (94) hide show
  1. package/{bin/util/css.js → css.js} +1 -1
  2. package/{bin/util/dynamic.d.ts → dynamic.d.ts} +2 -5
  3. package/{bin/util/dynamic.js → dynamic.js} +7 -5
  4. package/{bin/util/endpoint.d.ts → endpoint.d.ts} +2 -2
  5. package/{bin/util/endpoint.js → endpoint.js} +3 -1
  6. package/event-source.d.ts +26 -0
  7. package/event-source.js +123 -0
  8. package/example/eventdim-react/package.json +67 -0
  9. package/example/eventdim-react/server.js +90 -0
  10. package/example/island-react/global.d.ts +8 -0
  11. package/example/island-react/package.json +38 -0
  12. package/example/island-react/server.js +58 -0
  13. package/global.d.ts +7 -0
  14. package/index.d.ts +19 -0
  15. package/index.js +2 -0
  16. package/internal/cli/config.d.ts +13 -0
  17. package/internal/cli/config.js +11 -0
  18. package/internal/cli/index.js +15 -0
  19. package/internal/client.d.ts +1 -0
  20. package/{bin/client/entry.js → internal/client.js} +3 -1
  21. package/internal/compile/manifest.d.ts +1 -0
  22. package/{bin/client/index.js → internal/compile/manifest.js} +111 -65
  23. package/internal/compile/router.d.ts +1 -0
  24. package/internal/compile/router.js +51 -0
  25. package/internal/component/dynamic.d.ts +4 -0
  26. package/internal/component/dynamic.js +18 -0
  27. package/internal/component/head.d.ts +5 -0
  28. package/internal/component/head.js +22 -0
  29. package/internal/component/scripts.d.ts +4 -0
  30. package/internal/component/scripts.js +23 -0
  31. package/{bin/client → internal}/mount.js +9 -44
  32. package/internal/request/http.d.ts +10 -0
  33. package/internal/request/http.js +61 -0
  34. package/{bin → internal}/request/index.d.ts +3 -3
  35. package/internal/request/index.js +8 -0
  36. package/{bin → internal}/request/native.d.ts +2 -2
  37. package/internal/request/native.js +48 -0
  38. package/{bin/helper.d.ts → internal/util.d.ts} +2 -0
  39. package/{bin/helper.js → internal/util.js} +15 -0
  40. package/package.json +9 -5
  41. package/readme.md +2 -214
  42. package/{bin/request → request}/http.d.ts +1 -1
  43. package/{bin/request → request}/http.js +22 -4
  44. package/request/index.d.ts +13 -0
  45. package/request/index.js +3 -0
  46. package/request/native.d.ts +9 -0
  47. package/{bin/request → request}/native.js +2 -2
  48. package/{bin/util/response.d.ts → response.d.ts} +3 -1
  49. package/{bin/util/response.js → response.js} +5 -5
  50. package/{bin/router.d.ts → router.d.ts} +12 -10
  51. package/{bin/router.js → router.js} +61 -47
  52. package/{bin/util/shell.js → shell.js} +3 -1
  53. package/{bin/util → util}/parameters.d.ts +0 -3
  54. package/{bin/util → util}/parameters.js +0 -3
  55. package/{bin/util → util}/path-builder.js +2 -0
  56. package/util/route.d.ts +2 -0
  57. package/util/route.js +58 -0
  58. package/vite/bundle-splitter.d.ts +4 -0
  59. package/vite/bundle-splitter.js +26 -0
  60. package/vite/client-island.d.ts +4 -0
  61. package/vite/client-island.js +14 -0
  62. package/vite/code-splitting.d.ts +4 -0
  63. package/vite/code-splitting.js +14 -0
  64. package/vite/index.d.ts +3 -0
  65. package/vite/index.js +3 -0
  66. package/vite/router.d.ts +2 -0
  67. package/vite/router.js +29 -0
  68. package/bin/cli/config.d.ts +0 -10
  69. package/bin/cli/config.js +0 -4
  70. package/bin/cli/index.js +0 -66
  71. package/bin/client/entry.d.ts +0 -1
  72. package/bin/client/index.d.ts +0 -7
  73. package/bin/client/watch.d.ts +0 -1
  74. package/bin/client/watch.js +0 -11
  75. package/bin/index.d.ts +0 -11
  76. package/bin/index.js +0 -18
  77. package/bin/request/index.js +0 -6
  78. package/bin/response.d.ts +0 -9
  79. package/bin/response.js +0 -46
  80. package/bin/types.d.ts +0 -10
  81. package/bin/types.js +0 -1
  82. package/bin/util/event-source.d.ts +0 -16
  83. package/bin/util/event-source.js +0 -85
  84. package/bin/util/index.d.ts +0 -1
  85. package/bin/util/index.js +0 -7
  86. /package/{bin/util/cookies.d.ts → cookies.d.ts} +0 -0
  87. /package/{bin/util/cookies.js → cookies.js} +0 -0
  88. /package/{bin/util/css.d.ts → css.d.ts} +0 -0
  89. /package/{bin → internal}/cli/index.d.ts +0 -0
  90. /package/{bin/util → internal}/hash.d.ts +0 -0
  91. /package/{bin/util → internal}/hash.js +0 -0
  92. /package/{bin/client → internal}/mount.d.ts +0 -0
  93. /package/{bin/util/shell.d.ts → shell.d.ts} +0 -0
  94. /package/{bin/util → util}/path-builder.d.ts +0 -0
@@ -1,27 +1,14 @@
1
- /**
2
- * Builds the SSR and client side mounter for client components
3
- */
4
- import { readFile, writeFile } from "fs/promises";
5
1
  import { init, parse } from "es-module-lexer";
6
- import { QuickHash } from "../util/hash.js";
7
- import { CutString } from "../helper.js";
8
- const pivot = `\n// DO NOT EDIT BELOW THIS LINE\n`;
9
- export async function GenerateClient(config, force = false) {
10
- const file = await readFile(config.source, "utf8");
11
- const [source, history] = CutString(file, pivot);
12
- const hash = QuickHash(source);
13
- if (!force && ExtractHash(history) === hash)
14
- return;
15
- await init;
2
+ import { CutString, ServerOnlyWarning } from "../util.js";
3
+ ServerOnlyWarning("manifest-compiler");
4
+ export function CompileManifest(adapter, source, ssr) {
16
5
  const imported = ParseImports(source);
17
- await Promise.all([
18
- writeFile(config.source, source
19
- + pivot
20
- + `// hash: ${hash}\n`
21
- + BuildClientServer(config.adapter, imported)),
22
- writeFile(CutString(config.source, ".", -1)[0] + ".manifest.tsx", BuildClientManifest(config.adapter, imported))
23
- ]);
6
+ if (ssr)
7
+ return BuildServerManifest(adapter, imported);
8
+ return BuildClientManifest(adapter, imported);
9
+ ;
24
10
  }
11
+ await init; // ensure the webassembly module is ready
25
12
  function ParseImports(source) {
26
13
  const parsed = parse(source)[0];
27
14
  const out = [];
@@ -31,6 +18,8 @@ function ParseImports(source) {
31
18
  if (imported.t !== 1)
32
19
  continue;
33
20
  const href = source.slice(imported.s, imported.e);
21
+ if (href === "htmx-router")
22
+ continue;
34
23
  const front = source.slice(imported.ss, imported.s);
35
24
  const start = front.indexOf("{");
36
25
  if (start === -1) {
@@ -39,18 +28,20 @@ function ParseImports(source) {
39
28
  continue;
40
29
  }
41
30
  const end = front.lastIndexOf("}");
42
- const segments = front.slice(start + 1, end).split(",");
31
+ const middle = front.slice(start + 1, end);
32
+ const segments = middle.split(",");
43
33
  out.push({ mapping: segments.map(ExtractName), href });
44
34
  }
45
35
  return out;
46
36
  }
47
- function SafeScript(type, script) {
48
- switch (type) {
49
- case "react": return `<script dangerouslySetInnerHTML={{__html: ${script}}}></script>`;
50
- default: return `<script>${script}</script>`;
51
- }
37
+ function ExtractName(str) {
38
+ const parts = CutString(str, " as ");
39
+ if (parts[1].length !== 0)
40
+ return { name: parts[1].trim(), original: parts[0].trim() };
41
+ const name = parts[0].trim();
42
+ return { name, original: name };
52
43
  }
53
- function BuildClientServer(type, imported) {
44
+ function BuildServerManifest(type, imported) {
54
45
  const names = new Array();
55
46
  for (const imp of imported) {
56
47
  if (Array.isArray(imp.mapping))
@@ -58,7 +49,27 @@ function BuildClientServer(type, imported) {
58
49
  else
59
50
  names.push(imp.mapping.name);
60
51
  }
61
- let out = `import { StyleClass } from "htmx-router";\n`
52
+ let out = "";
53
+ for (const imp of imported) {
54
+ out += "import ";
55
+ if (!Array.isArray(imp.mapping)) {
56
+ out += ImportNameSource(imp.mapping) + " ";
57
+ }
58
+ else {
59
+ let first = true;
60
+ out += "{ ";
61
+ for (const name of imp.mapping) {
62
+ if (first)
63
+ first = false;
64
+ else
65
+ out += ", ";
66
+ out += ImportNameSource(name);
67
+ }
68
+ out += " } ";
69
+ }
70
+ out += `"${imp.href}";\n\n`;
71
+ }
72
+ out = `import { StyleClass } from "htmx-router/css";\n`
62
73
  + `const island = new StyleClass("i", ".this{display:contents;}\\n").name;\n\n`
63
74
  + "type FirstArg<T> = T extends (arg: infer U, ...args: any[]) => any ? U : never;\n"
64
75
  + "function mount(name: string, data: string, ssr?: JSX.Element) {\n"
@@ -75,58 +86,93 @@ function BuildClientServer(type, imported) {
75
86
  + `\t\treturn mount("${name}", JSON.stringify(rest), children);\n`
76
87
  + `\t},\n`;
77
88
  }
78
- out += "}\nexport default Client;\n\n"
79
- + `import { __RebuildClient__ } from "htmx-router/bin/client/watch.js";\n`
80
- + `__RebuildClient__();`;
89
+ out += "}\nexport default Client;";
81
90
  return out;
82
91
  }
83
- const renderer = {
84
- react: '\t\tconst r = await import("react-dom/client");\n'
85
- + "\t\tr.createRoot(element).render(<C {...props} />);\n"
86
- };
87
- function BuildClientManifest(type, imports) {
88
- let out = "/*------------------------------------------\n"
89
- + " * Generated by htmx-router *\n"
90
- + " * Warn: Any changes will be overwritten *\n"
91
- + "-------------------------------------------*/\n\n"
92
- + "/* eslint-disable @typescript-eslint/no-explicit-any */\n"
93
- + "const client = {\n";
94
- const render = renderer[type];
95
- if (!render) {
96
- console.error(`Unsupported client adapter ${type}`);
97
- process.exit(1);
92
+ function ImportNameSource(name) {
93
+ if (name.original === name.name)
94
+ return name;
95
+ return `${name.original} as ${name.name}`;
96
+ }
97
+ function SafeScript(type, script) {
98
+ switch (type) {
99
+ case "react": return `<script dangerouslySetInnerHTML={{__html: ${script}}}></script>`;
100
+ default: return `<script>${script}</script>`;
98
101
  }
102
+ }
103
+ function BuildClientManifest(type, imports) {
104
+ const bind = binding[type];
105
+ if (!bind)
106
+ throw new Error(`Unsupported client adapter ${type}`);
107
+ let out = "const client = {\n";
99
108
  for (const imported of imports) {
100
109
  if (Array.isArray(imported.mapping)) {
101
110
  for (const map of imported.mapping) {
102
111
  out += `\t${map.name}: async (element: HTMLElement, props: any) => {\n`
103
112
  + `\t\tconst C = (await import("${imported.href}")).${map.original};\n`
104
- + render
105
- + `\t},\n`;
113
+ + bind.mount
114
+ + `\n\t},\n`;
106
115
  }
107
116
  }
108
117
  else {
109
118
  out += `\t${imported.mapping.name}: async (element: HTMLElement, props: any) => {\n`
110
119
  + `\t\tconst C = (await import("${imported.href}")).default;\n`
111
- + render
112
- + `\t},\n`;
120
+ + bind.mount
121
+ + `\n\t},\n`;
113
122
  }
114
123
  }
115
124
  out += "}\nexport default client;\n"
116
- + "(window as any).CLIENT = client;";
125
+ + "(window as any).CLIENT = client;\n\n";
126
+ out += bind.unmount;
127
+ out += cleanup;
117
128
  return out;
118
129
  }
119
- function ExtractName(str) {
120
- const parts = CutString(str, "as");
121
- if (parts[1].length !== 0)
122
- return { name: parts[1].trim(), original: parts[0].trim() };
123
- const name = parts[0].trim();
124
- return { name, original: name };
125
- }
126
- function ExtractHash(source) {
127
- const regex = /\/\/\s+hash\s*:\s*(\w+)/;
128
- const match = source.match(regex);
129
- if (match)
130
- return match[1] || "";
131
- return "";
130
+ const binding = {
131
+ react: {
132
+ mount: '\t\tconst d = await import("react-dom/client");\n'
133
+ + '\t\tconst r = d.createRoot(element);\n'
134
+ + '\t\tr.render(<C {...props} />);\n'
135
+ + '\t\tmounted.set(element, r);',
136
+ unmount: `
137
+ import type { Root } from "react-dom/client";
138
+ const mounted = new Map<HTMLElement, Root>();
139
+ function Unmount(node: HTMLElement, root: Root) {
140
+ mounted.delete(node);
141
+ root.unmount();
142
+ }`
143
+ }
144
+ };
145
+ const cleanup = `
146
+
147
+ const limbo = new Set<Node>();
148
+ let queued = false;
149
+ const observer = new MutationObserver((mutations) => {
150
+ for (const mut of mutations) {
151
+ for (const node of mut.removedNodes) limbo.add(node);
152
+ for (const node of mut.addedNodes) limbo.delete(node);
153
+ }
154
+
155
+ if (!queued) {
156
+ queueMicrotask(Cleanup);
157
+ queued = true;
158
+ }
159
+ });
160
+ observer.observe(document.body, { childList: true, subtree: true });
161
+
162
+ function Cleanup() {
163
+ queued = false;
164
+ for (const elm of limbo) CleanNode(elm);
165
+ limbo.clear();
132
166
  }
167
+
168
+ function CleanNode(node: Node) {
169
+ if (node instanceof HTMLElement) {
170
+ const root = mounted.get(node);
171
+ if (root) {
172
+ console.info("unmounting", node);
173
+ Unmount(node, root);
174
+ }
175
+ }
176
+
177
+ for (const child of node.childNodes) CleanNode(child);
178
+ }`;
@@ -0,0 +1 @@
1
+ export declare function CompileRouter(folder: string): string;
@@ -0,0 +1,51 @@
1
+ export function CompileRouter(folder) {
2
+ return `import { GenericContext, RouteTree } from "htmx-router/bin/router";
3
+ import { GetClientEntryURL } from 'htmx-router/bin/client/entry';
4
+ import { DynamicReference } from "htmx-router/bin/util/dynamic";
5
+ import { GetMountUrl } from 'htmx-router/bin/client/mount';
6
+ import { GetSheetUrl } from 'htmx-router/bin/util/css';
7
+ import { resolve } from "path";
8
+
9
+ const modules = import.meta.glob('/${folder}/**/*.{ts,tsx}', { eager: true });
10
+ console.log("${folder}");
11
+ console.log(modules);
12
+
13
+ export const tree = new RouteTree();
14
+ for (const path in modules) {
15
+ const mod = modules[path];
16
+ const tail = path.lastIndexOf(".");
17
+ const url = path.slice(${folder.length + 1}, tail);
18
+ tree.ingest(url, mod);
19
+ }`;
20
+ }
21
+ /*
22
+ export function Dynamic<T extends Record<string, string>>(props: {
23
+ params?: T,
24
+ loader: (ctx: GenericContext, params?: T) => Promise<JSX.Element>
25
+ children?: JSX.Element
26
+ }): JSX.Element {
27
+ return <div
28
+ hx-get={DynamicReference(props.loader, props.params)}
29
+ hx-trigger="load"
30
+ hx-swap="outerHTML transition:true"
31
+ style={{ display: "contents" }}
32
+ >{props.children ? props.children : ""}</div>
33
+ }
34
+
35
+ let headCache: JSX.Element | null = null;
36
+ const isProduction = process.env.NODE_ENV === "production";
37
+ const clientEntry = await GetClientEntryURL();
38
+ export function Scripts() {
39
+ if (headCache) return headCache;
40
+
41
+ const res = <>
42
+ <link href={GetSheetUrl()} rel="stylesheet"></link>
43
+ { isProduction ? "" : <script type="module" src="/@vite/client"></script> }
44
+ <script type="module" src={clientEntry}></script>
45
+ <script src={GetMountUrl()}></script>
46
+ </>;
47
+
48
+ if (isProduction) headCache = res;
49
+ return res;
50
+ }
51
+ */
@@ -0,0 +1,4 @@
1
+ declare const _default: {
2
+ "*": string;
3
+ };
4
+ export default _default;
@@ -0,0 +1,18 @@
1
+ const generic = `import { DynamicReference } from "htmx-router/dynamic";
2
+ import { GenericContext } from "htmx-router/router";
3
+
4
+ export function Dynamic<T extends Record<string, string>>(props: {
5
+ params?: T,
6
+ loader: (ctx: GenericContext, params?: T) => Promise<JSX.Element>
7
+ children?: JSX.Element
8
+ }): JSX.Element {
9
+ return <div
10
+ hx-get={DynamicReference(props.loader, props.params)}
11
+ hx-trigger="load"
12
+ hx-swap="outerHTML transition:true"
13
+ style={{ display: "contents" }}
14
+ >{props.children ? props.children : ""}</div>
15
+ }`;
16
+ export default {
17
+ "*": generic
18
+ };
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ "*": string;
3
+ react: string;
4
+ };
5
+ export default _default;
@@ -0,0 +1,22 @@
1
+ const generic = `import { RenderMetaDescriptor, ShellOptions } from "htmx-router/shell";
2
+
3
+ export function Head<T>(props: { options: ShellOptions<T>, children: JSX.Element }) {
4
+ return <head>
5
+ { RenderMetaDescriptor(props.options) as "safe" }
6
+ { props.children as "safe" }
7
+ </head>;
8
+ }`;
9
+ const react = `import { RenderMetaDescriptor, ShellOptions } from "htmx-router/shell";
10
+ import { renderToString } from 'react-dom/server';
11
+ import { ReactNode } from "react";
12
+
13
+ export function Head<T>(props: { options: ShellOptions<T>, children: ReactNode }) {
14
+ const body = RenderMetaDescriptor(props.options)
15
+ + renderToString(props.children);
16
+
17
+ return <head dangerouslySetInnerHTML={{ __html: body }}></head>;
18
+ }`;
19
+ export default {
20
+ "*": generic,
21
+ react
22
+ };
@@ -0,0 +1,4 @@
1
+ declare const _default: {
2
+ "*": string;
3
+ };
4
+ export default _default;
@@ -0,0 +1,23 @@
1
+ const generic = `import { GetClientEntryURL } from 'htmx-router/internal/client';
2
+ import { GetMountUrl } from 'htmx-router/internal/mount';
3
+ import { GetSheetUrl } from 'htmx-router/css';
4
+
5
+ let cache: JSX.Element | null = null;
6
+ const isProduction = process.env.NODE_ENV === "production";
7
+ const clientEntry = await GetClientEntryURL();
8
+ export function Scripts() {
9
+ if (cache) return cache;
10
+
11
+ const res = <>
12
+ <link href={GetSheetUrl()} rel="stylesheet"></link>
13
+ { isProduction ? "" : <script type="module" src="/@vite/client"></script> }
14
+ <script type="module" src={clientEntry}></script>
15
+ <script src={GetMountUrl()}></script>
16
+ </>;
17
+
18
+ if (isProduction) cache = res;
19
+ return res;
20
+ }`;
21
+ export default {
22
+ "*": generic
23
+ };
@@ -1,5 +1,6 @@
1
- import { QuickHash } from "../util/hash.js";
2
- import { CutString } from "../helper.js";
1
+ import { ServerOnlyWarning } from "./util.js";
2
+ ServerOnlyWarning("client-mounter");
3
+ import { CutString, QuickHash } from "./util.js";
3
4
  // this function simply exists so it can be stringified and written into the client js bundle
4
5
  function ClientMounter() {
5
6
  const theme = {
@@ -32,7 +33,10 @@ function ClientMounter() {
32
33
  const global = window;
33
34
  const mountRequests = new Array();
34
35
  function RequestMount(funcName, json) {
35
- mountRequests.push([funcName, document.currentScript.previousElementSibling, json]);
36
+ const elm = document.currentScript.previousElementSibling;
37
+ if (elm.hasAttribute("mounted"))
38
+ return;
39
+ mountRequests.push([funcName, elm, json]);
36
40
  }
37
41
  function Mount() {
38
42
  if (mountRequests.length < 1)
@@ -45,51 +49,12 @@ function ClientMounter() {
45
49
  if (!func)
46
50
  throw new Error(`Component ${funcName} is missing from client manifest`);
47
51
  func(element, json);
52
+ element.setAttribute("mounted", "yes");
48
53
  }
49
54
  mountRequests.length = 0;
50
55
  }
51
56
  document.addEventListener("DOMContentLoaded", Mount);
52
- if (global.htmx)
53
- global.htmx.onLoad(Mount);
54
- // Track the number of active requests
55
- let activeRequests = 0;
56
- const updateLoadingAttribute = () => {
57
- if (activeRequests > 0)
58
- document.body.setAttribute('data-loading', 'true');
59
- else
60
- document.body.removeAttribute('data-loading');
61
- };
62
- const originalXHROpen = XMLHttpRequest.prototype.open;
63
- const originalXHRSend = XMLHttpRequest.prototype.send;
64
- // @ts-ignore
65
- XMLHttpRequest.prototype.open = function (...args) {
66
- this.addEventListener('loadstart', () => {
67
- activeRequests++;
68
- updateLoadingAttribute();
69
- });
70
- this.addEventListener('loadend', () => {
71
- activeRequests--;
72
- updateLoadingAttribute();
73
- });
74
- originalXHROpen.apply(this, args);
75
- };
76
- XMLHttpRequest.prototype.send = function (...args) {
77
- originalXHRSend.apply(this, args);
78
- };
79
- // Override fetch
80
- const originalFetch = window.fetch;
81
- window.fetch = async (...args) => {
82
- activeRequests++;
83
- updateLoadingAttribute();
84
- try {
85
- const response = await originalFetch(...args);
86
- return response;
87
- }
88
- finally {
89
- activeRequests--;
90
- updateLoadingAttribute();
91
- }
92
- };
57
+ document.addEventListener("htmx:load", Mount);
93
58
  return {
94
59
  mountAboveWith: RequestMount,
95
60
  theme
@@ -0,0 +1,10 @@
1
+ import type { IncomingMessage, ServerResponse } from "http";
2
+ import type { ViteDevServer } from "vite";
3
+ import type { GenericContext } from "../../router.js";
4
+ type Config = {
5
+ build: Promise<any> | (() => Promise<Record<string, any>>);
6
+ viteDevServer: ViteDevServer | null;
7
+ render: GenericContext["render"];
8
+ };
9
+ export declare function createRequestHandler(config: Config): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
10
+ export {};
@@ -0,0 +1,61 @@
1
+ import { ServerOnlyWarning } from "../util.js";
2
+ ServerOnlyWarning("http-request");
3
+ import { Resolve } from "./native.js";
4
+ export function createRequestHandler(config) {
5
+ return async (req, res) => {
6
+ try {
7
+ const mod = typeof config.build === "function" ? await config.build() : await config.build;
8
+ const request = NativeRequest(req);
9
+ let { response, headers } = await Resolve(request, mod.tree, config);
10
+ res.writeHead(response.status, headers);
11
+ if (response.body instanceof ReadableStream) {
12
+ const reader = response.body.getReader();
13
+ while (true) {
14
+ const { done, value } = await reader.read();
15
+ if (done)
16
+ break;
17
+ res.write(value); // `value` is a Uint8Array.
18
+ }
19
+ res.end();
20
+ }
21
+ else {
22
+ const rendered = await response.text();
23
+ res.end(rendered);
24
+ }
25
+ }
26
+ catch (e) {
27
+ res.statusCode = 500;
28
+ if (e instanceof Error) {
29
+ console.error(e.stack);
30
+ config.viteDevServer?.ssrFixStacktrace(e);
31
+ res.end(e.stack);
32
+ }
33
+ else {
34
+ console.error(e);
35
+ res.end(String(e));
36
+ }
37
+ }
38
+ };
39
+ }
40
+ function NativeRequest(req) {
41
+ const ctrl = new AbortController();
42
+ const headers = new Headers(req.headers);
43
+ const url = new URL(`http://${headers.get('host')}${req.originalUrl || req.url}`);
44
+ req.once('aborted', () => ctrl.abort());
45
+ const bodied = req.method !== "GET" && req.method !== "HEAD";
46
+ const request = new Request(url, {
47
+ headers,
48
+ method: req.method,
49
+ body: bodied ? req : undefined,
50
+ signal: ctrl.signal,
51
+ referrer: headers.get("referrer") || undefined,
52
+ // @ts-ignore
53
+ duplex: bodied ? 'half' : undefined
54
+ });
55
+ if (!request.headers.has("X-Real-IP")) {
56
+ const info = req.socket.address();
57
+ if ("address" in info)
58
+ request.headers.set("X-Real-IP", info.address);
59
+ }
60
+ return request;
61
+ }
@@ -1,7 +1,7 @@
1
1
  import type { ViteDevServer } from "vite";
2
- import * as native from "../request/native.js";
3
- import * as http from "../request/http.js";
4
- import { GenericContext, RouteTree } from '../router.js';
2
+ import type { GenericContext, RouteTree } from '../../router.js';
3
+ import * as native from "./native.js";
4
+ import * as http from "./http.js";
5
5
  export type Config = {
6
6
  build: Promise<any> | (() => Promise<Record<string, any>>);
7
7
  viteDevServer: ViteDevServer | null;
@@ -0,0 +1,8 @@
1
+ import { ServerOnlyWarning } from "../util.js";
2
+ ServerOnlyWarning("request");
3
+ import * as native from "./native.js";
4
+ import * as http from "./http.js";
5
+ export const createRequestHandler = {
6
+ http: http.createRequestHandler,
7
+ native: native.createRequestHandler
8
+ };
@@ -1,5 +1,5 @@
1
- import { RouteTree } from '../router.js';
2
- import { Config } from '../request/index.js';
1
+ import { type RouteTree } from '../../router.js';
2
+ import type { Config } from './index.js';
3
3
  export declare function createRequestHandler(config: Config): (req: Request) => Promise<Response>;
4
4
  export declare function Resolve(request: Request, tree: RouteTree, config: Config): Promise<{
5
5
  response: Response;
@@ -0,0 +1,48 @@
1
+ import { ServerOnlyWarning } from "../util.js";
2
+ ServerOnlyWarning("native-request");
3
+ import { GenericContext } from '../../router.js';
4
+ export function createRequestHandler(config) {
5
+ return async (req) => {
6
+ try {
7
+ const mod = typeof config.build === "function" ? await config.build() : await config.build;
8
+ let { response } = await Resolve(req, mod.tree, config);
9
+ return response;
10
+ }
11
+ catch (e) {
12
+ if (e instanceof Error) {
13
+ console.error(e.stack);
14
+ config.viteDevServer?.ssrFixStacktrace(e);
15
+ return new Response(e.message + "\n" + e.stack, { status: 500, statusText: "Internal Server Error" });
16
+ }
17
+ else {
18
+ console.error(e);
19
+ return new Response(String(e), { status: 500, statusText: "Internal Server Error" });
20
+ }
21
+ }
22
+ };
23
+ }
24
+ export async function Resolve(request, tree, config) {
25
+ const url = new URL(request.url);
26
+ const ctx = new GenericContext(request, url, config.render);
27
+ const x = ctx.url.pathname.endsWith("/") ? ctx.url.pathname.slice(0, -1) : ctx.url.pathname;
28
+ const fragments = x.split("/").slice(1);
29
+ let response = await tree.resolve(fragments, ctx);
30
+ if (response === null)
31
+ response = new Response("No Route Found", { status: 404, statusText: "Not Found", headers: ctx.headers });
32
+ // Override with context headers
33
+ if (response.headers !== ctx.headers) {
34
+ for (const [key, value] of ctx.headers) {
35
+ if (response.headers.has(key))
36
+ continue;
37
+ response.headers.set(key, value);
38
+ }
39
+ }
40
+ // Merge cookie changes
41
+ const headers = Object.fromEntries(response.headers);
42
+ const cookies = ctx.cookie.export();
43
+ if (cookies.length > 0) {
44
+ headers['set-cookie'] = cookies;
45
+ response.headers.set("Set-Cookie", cookies[0]); // Response object doesn't support multi-header..[]
46
+ }
47
+ return { response, headers };
48
+ }
@@ -1,2 +1,4 @@
1
+ export declare function QuickHash(input: string): string;
1
2
  export declare function CutString(str: string, pivot: string, offset?: number): [string, string];
2
3
  export declare function Singleton<T>(name: string, cb: () => T): T;
4
+ export declare function ServerOnlyWarning(context: string): void;
@@ -1,3 +1,10 @@
1
+ export function QuickHash(input) {
2
+ let hash = 0;
3
+ for (let i = 0; i < input.length; i++) {
4
+ hash = (hash * 31 + input.charCodeAt(i)) >>> 0;
5
+ }
6
+ return hash.toString(36).slice(0, 5);
7
+ }
1
8
  export function CutString(str, pivot, offset = 1) {
2
9
  if (offset > 0) {
3
10
  let cursor = 0;
@@ -32,3 +39,11 @@ export function Singleton(name, cb) {
32
39
  g.__singletons[name] ??= cb();
33
40
  return g.__singletons[name];
34
41
  }
42
+ export function ServerOnlyWarning(context) {
43
+ if (typeof process !== "undefined")
44
+ return;
45
+ if (typeof document == "undefined")
46
+ return;
47
+ console.warn(`Warn: Server-side only htmx-router feature ${context} has leaked to client code`);
48
+ console.log(typeof document, typeof process);
49
+ }
package/package.json CHANGED
@@ -1,15 +1,19 @@
1
1
  {
2
2
  "name": "htmx-router",
3
- "version": "1.0.0-alpha.5",
3
+ "version": "1.0.0-alpha.6",
4
4
  "description": "A simple SSR framework with dynamic+client islands",
5
- "keywords": ["htmx", "router", ""],
6
- "main": "./bin/index.js",
5
+ "keywords": [
6
+ "htmx",
7
+ "router",
8
+ ""
9
+ ],
10
+ "main": "./index.js",
7
11
  "type": "module",
8
12
  "scripts": {
9
13
  "build": "tsc && tsc-alias"
10
14
  },
11
15
  "bin": {
12
- "htmx-router": "bin/cli/index.js"
16
+ "htmx-router": "./cli/index.js"
13
17
  },
14
18
  "repository": {
15
19
  "type": "git",
@@ -23,7 +27,7 @@
23
27
  "homepage": "https://github.com/AjaniBilby/htmx-router#readme",
24
28
  "dependencies": {
25
29
  "es-module-lexer": "^1.5.4",
26
- "vite": "^6.0.1"
30
+ "vite": "^6.0.6"
27
31
  },
28
32
  "devDependencies": {
29
33
  "@types/node": "^20.4.5",