htmx-router 1.0.0-pre4 → 1.0.0

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/cookies.js CHANGED
@@ -47,8 +47,9 @@ export class Cookies {
47
47
  options.path ||= "/";
48
48
  this.config[name] = options;
49
49
  this.map[name] = value;
50
- if (typeof this.source === "object")
50
+ if (this.source !== null && typeof this.source === "object") {
51
51
  document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
52
+ }
52
53
  }
53
54
  unset(name) {
54
55
  this.parse();
package/defer.js CHANGED
@@ -75,6 +75,6 @@ export async function loader(ctx) {
75
75
  if (res === null)
76
76
  return null;
77
77
  ctx.headers.set("X-Partial", "true");
78
- return ctx.render(res);
78
+ return ctx.render(res, ctx.headers);
79
79
  }
80
80
  export const action = loader;
package/endpoint.js CHANGED
@@ -35,6 +35,6 @@ export async function loader(ctx) {
35
35
  return null;
36
36
  if (res instanceof Response)
37
37
  return res;
38
- return ctx.render(res);
38
+ return ctx.render(res, ctx.headers);
39
39
  }
40
40
  export const action = loader;
@@ -72,18 +72,20 @@ function BuildServerManifest(type, imported) {
72
72
  out += `\nimport { Style } from "htmx-router/css";\n`
73
73
  + `const island = new Style("i", ".this{display:contents;}\\n").name;\n\n`
74
74
  + "type FirstArg<T> = T extends (arg: infer U, ...args: any[]) => any ? U : never;\n"
75
- + "function mount(name: string, data: string, ssr?: JSX.Element) {\n"
76
- + "\treturn (<>\n"
77
- + `\t\t<div className={island}>{ssr}</div>\n`
78
- + `\t\t${SafeScript(type, "`Router.mountAboveWith('${name}', ${data})`")}\n`
79
- + "\t</>);\n"
75
+ + "function mount(name: string, json: string, ssr?: JSX.Element) {\n"
76
+ + "\treturn (<div className={island}>\n"
77
+ + `\t\t{ssr}\n`
78
+ + `\t\t${SafeScript(type, "`Router.mountParentWith('${name}', ${json})`")}\n`
79
+ + "\t</div>);\n"
80
80
  + "}\n"
81
- + "\n"
81
+ + "function Stringify(data: any) {\n"
82
+ + "\treturn JSON.stringify(data).replaceAll('<', '\\x3C');\n"
83
+ + "}\n\n"
82
84
  + "const Client = {\n";
83
85
  for (const name of names) {
84
86
  out += `\t${name}: function(props: FirstArg<typeof ${name}> & { children?: JSX.Element }) {\n`
85
- + `\t\tconst { children, ...rest } = props;\n`
86
- + `\t\treturn mount("${name}", JSON.stringify(rest), children);\n`
87
+ + `\t\tconst { children, ...data } = props;\n`
88
+ + `\t\treturn mount("${name}", Stringify(data), children);\n`
87
89
  + `\t},\n`;
88
90
  }
89
91
  out += "}\nexport default Client;";
package/internal/mount.js CHANGED
@@ -33,30 +33,68 @@ function ClientMounter() {
33
33
  const global = window;
34
34
  const mountRequests = new Array();
35
35
  function RequestMount(funcName, json) {
36
- const elm = document.currentScript.previousElementSibling;
36
+ const elm = document.currentScript.parentElement;
37
37
  if (elm.hasAttribute("mounted"))
38
38
  return;
39
+ if (!document.body.contains(elm))
40
+ return;
39
41
  mountRequests.push([funcName, elm, json]);
40
42
  }
41
- function Mount() {
42
- if (mountRequests.length < 1)
43
- return;
43
+ function Mount([funcName, element, json]) {
44
+ console.info("hydrating", funcName, "into", element);
45
+ const func = global.CLIENT[funcName];
46
+ if (!func)
47
+ throw new Error(`Component ${funcName} is missing from client manifest`);
48
+ func(element, json);
49
+ element.setAttribute("mounted", "yes");
50
+ }
51
+ function MountAll() {
44
52
  if (!global.CLIENT)
45
53
  throw new Error("Client manifest missing");
46
- for (const [funcName, element, json] of mountRequests) {
47
- console.info("hydrating", funcName, "into", element);
48
- const func = global.CLIENT[funcName];
49
- if (!func)
50
- throw new Error(`Component ${funcName} is missing from client manifest`);
51
- func(element, json);
52
- element.setAttribute("mounted", "yes");
54
+ if (mountRequests.length < 1)
55
+ return;
56
+ for (const request of mountRequests) {
57
+ if (!document.body.contains(request[1]))
58
+ continue;
59
+ Mount(request);
53
60
  }
54
61
  mountRequests.length = 0;
55
62
  }
56
- document.addEventListener("DOMContentLoaded", Mount);
57
- document.addEventListener("htmx:load", Mount);
63
+ function MountStep() {
64
+ let request = mountRequests.shift();
65
+ while (request && !document.body.contains(request[1]))
66
+ request = mountRequests.shift();
67
+ if (!request) {
68
+ console.warn("No more pending mount requests");
69
+ return;
70
+ }
71
+ Mount(request);
72
+ }
73
+ function Freeze() {
74
+ localStorage.setItem(freezeKey, "frozen");
75
+ window.location.reload();
76
+ }
77
+ function Unfreeze() {
78
+ localStorage.removeItem(freezeKey);
79
+ window.location.reload();
80
+ }
81
+ const freezeKey = "htmx-mount-freeze";
82
+ if (localStorage.getItem(freezeKey)) {
83
+ const ok = confirm("Client mounting is frozen, do you want to unfreeze");
84
+ if (ok)
85
+ Unfreeze();
86
+ }
87
+ else {
88
+ document.addEventListener("DOMContentLoaded", MountAll);
89
+ document.addEventListener("htmx:load", MountAll);
90
+ }
58
91
  return {
59
- mountAboveWith: RequestMount,
92
+ mountParentWith: RequestMount,
93
+ mount: {
94
+ freeze: Freeze,
95
+ unfreeze: Unfreeze,
96
+ step: MountStep
97
+ },
60
98
  theme
61
99
  };
62
100
  }
@@ -9,7 +9,7 @@ export declare class GenericContext {
9
9
  [key: string]: string;
10
10
  };
11
11
  url: URL;
12
- render: (res: JSX.Element) => Response;
12
+ render: (res: JSX.Element, headers: Headers) => Promise<Response> | Response;
13
13
  constructor(request: GenericContext["request"], url: GenericContext["url"], renderer: GenericContext["render"]);
14
14
  shape<T extends ParameterShaper>(shape: T): RouteContext<T>;
15
15
  }
package/navigate.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function navigate(href: string, pushUrl?: boolean): Promise<void>;
2
+ export declare function revalidate(): Promise<void>;
3
+ export declare function htmxAppend(href: string, verb?: string): Promise<void>;
package/navigate.js ADDED
@@ -0,0 +1,31 @@
1
+ function htmx() {
2
+ const htmx = window.htmx;
3
+ if (typeof htmx !== "object")
4
+ throw new Error("Missing htmx");
5
+ return htmx;
6
+ }
7
+ export async function navigate(href, pushUrl = true) {
8
+ if (typeof window !== "object")
9
+ return;
10
+ const url = new URL(href, window.location.href);
11
+ if (url.host !== window.location.host) {
12
+ window.location.assign(href);
13
+ return;
14
+ }
15
+ // Perform an HTMX GET request similar to hx-boost
16
+ await htmx().ajax("GET", href, {
17
+ target: 'body',
18
+ swap: 'outerHTML',
19
+ history: pushUrl
20
+ });
21
+ }
22
+ export function revalidate() {
23
+ return navigate("", false);
24
+ }
25
+ export async function htmxAppend(href, verb = "GET") {
26
+ await htmx().ajax(verb, href, {
27
+ target: 'body',
28
+ swap: 'beforeend',
29
+ history: false
30
+ });
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmx-router",
3
- "version": "1.0.0-pre4",
3
+ "version": "1.0.0",
4
4
  "description": "A lightweight SSR framework with server+client islands",
5
5
  "keywords": [
6
6
  "htmx", "router", "client islands", "ssr", "vite"
package/router.d.ts CHANGED
@@ -12,7 +12,7 @@ export declare class RouteContext<T extends ParameterShaper = {}> {
12
12
  readonly cookie: Cookies;
13
13
  readonly params: Parameterized<T>;
14
14
  readonly url: URL;
15
- render: (res: JSX.Element) => Response;
15
+ render: GenericContext["render"];
16
16
  constructor(base: GenericContext | RouteContext, params: ParameterPrelude<T>, shape: T);
17
17
  }
18
18
  export declare class RouteTree {
package/router.js CHANGED
@@ -173,7 +173,7 @@ class RouteLeaf {
173
173
  return null;
174
174
  if (res instanceof Response)
175
175
  return res;
176
- return ctx.render(res);
176
+ return await ctx.render(res, ctx.headers);
177
177
  }
178
178
  async error(ctx, e) {
179
179
  if (!this.module.error)
@@ -181,7 +181,7 @@ class RouteLeaf {
181
181
  const res = await this.module.error(ctx, e);
182
182
  if (res instanceof Response)
183
183
  return res;
184
- return ctx.render(res);
184
+ return await ctx.render(res, ctx.headers);
185
185
  }
186
186
  async renderWrapper(ctx) {
187
187
  try {
@@ -1,8 +1,9 @@
1
1
  import { ServerOnlyWarning } from "../internal/util.js";
2
2
  ServerOnlyWarning("bundle-splitter");
3
+ import { init, parse } from "es-module-lexer";
4
+ await init;
3
5
  const serverPattern = /\.server\.[tj]s(x)?/;
4
6
  const clientPattern = /\.client\.[tj]s(x)?/;
5
- const BLANK_MODULE = "export {};";
6
7
  export function BundleSplitter() {
7
8
  return {
8
9
  name: "htmx-bundle-splitter",
@@ -11,16 +12,27 @@ export function BundleSplitter() {
11
12
  const ssr = options?.ssr || false;
12
13
  const pattern = ssr ? clientPattern : serverPattern;
13
14
  if (pattern.test(id))
14
- return BLANK_MODULE;
15
+ return StubExports(code);
15
16
  if (ssr) {
16
17
  if (code.startsWith('"use client"'))
17
- return BLANK_MODULE;
18
+ return StubExports(code);
18
19
  }
19
20
  else {
20
21
  if (code.startsWith('"use server"'))
21
- return BLANK_MODULE;
22
+ return StubExports(code);
22
23
  }
23
24
  return code;
24
25
  }
25
26
  };
26
27
  }
28
+ // A server only module may be imported into client code,
29
+ // But as long as it isn't used this shouldn't break the program.
30
+ // However JS will crash the import if the export name it's looking for isn't present.
31
+ // Even if it's never used.
32
+ // So we must place some stubs just in case
33
+ function StubExports(code) {
34
+ const exports = parse(code)[1];
35
+ return exports.map(x => x.n === "default"
36
+ ? "export default undefined;"
37
+ : `export const ${x.n} = undefined;`).join("\n");
38
+ }