akanjs 3.1.0-rc.6 → 3.1.0-rc.7

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/client/href.ts ADDED
@@ -0,0 +1,24 @@
1
+ export class Href {
2
+ static readonly #blocked = new Set(["javascript:", "vbscript:", "data:"]);
3
+
4
+ /** External is what the browser takes off-site: any absolute URL with a scheme, and `//`/`\\` network-path
5
+ * forms it resolves to another host. javascript:, vbscript: and data: never count. */
6
+ static isExternal(href: string): boolean {
7
+
8
+ if (URL.canParse(href)) return !Href.#blocked.has(new URL(href).protocol);
9
+
10
+ return /^[\\/]{2}/.test(href.replace(/[\t\n\r]/g, "").replace(/^[\u0000-\u0020]+/, ""));
11
+ }
12
+
13
+ /** An explicit http(s) scheme in any case; `//host` and `httpbin/x` do not qualify. */
14
+ static hasWebScheme(href: string): boolean {
15
+ return /^https?:/i.test(href);
16
+ }
17
+
18
+ /** Canonical absolute URL for an external href: scheme-relative and backslash hosts resolve against the page's
19
+ * http(s) origin, or https: when the page is a native `app://` shell. */
20
+ static toExternalUrl(href: string, base: string): string {
21
+ const baseProtocol = URL.canParse(base) ? new URL(base).protocol : "";
22
+ return new URL(href, /^https?:$/.test(baseProtocol) ? base : "https://localhost").href;
23
+ }
24
+ }
package/client/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./csrTypes";
7
7
  export * from "./decorators";
8
8
  export * from "./device";
9
9
  export * from "./frameConfig";
10
+ export * from "./href";
10
11
  export * from "./locale";
11
12
  export * from "./makePageProto";
12
13
  export * from "./mcpView";
package/client/router.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getEnv } from "akanjs/base";
2
2
  import { getBasePathFromPathname, Logger, parseAkanI18nEnv, parseBasePaths, trimSlashes } from "akanjs/common";
3
3
  import { csrLocation } from "./csrLocation";
4
+ import { Href } from "./href";
4
5
  import { isMcpView } from "./mcpView";
5
6
 
6
7
  export interface RouteOptions {
@@ -158,7 +159,8 @@ export const getPathInfo = (requestUrl: string, lang: string, prefix: string) =>
158
159
  ? pathWithSubRoute.slice(prefixLength)
159
160
  : pathWithSubRoute;
160
161
  const subRoute = prefix ? `/${prefix}` : "";
161
- const pathname = path.startsWith("http") ? path : path === "/" ? `/${lang}${subRoute}` : `/${lang}${subRoute}${path}`;
162
+
163
+ const pathname = Href.hasWebScheme(path) ? path : path === "/" ? `/${lang}${subRoute}` : `/${lang}${subRoute}${path}`;
162
164
  const href = `${pathname}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`;
163
165
  return { path, pathname, hash, search, href };
164
166
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.1.0-rc.6",
3
+ "version": "3.1.0-rc.7",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -0,0 +1,11 @@
1
+ export declare class Href {
2
+ #private;
3
+ /** External is what the browser takes off-site: any absolute URL with a scheme, and `//`/`\\` network-path
4
+ * forms it resolves to another host. javascript:, vbscript: and data: never count. */
5
+ static isExternal(href: string): boolean;
6
+ /** An explicit http(s) scheme in any case; `//host` and `httpbin/x` do not qualify. */
7
+ static hasWebScheme(href: string): boolean;
8
+ /** Canonical absolute URL for an external href: scheme-relative and backslash hosts resolve against the page's
9
+ * http(s) origin, or https: when the page is a native `app://` shell. */
10
+ static toExternalUrl(href: string, base: string): string;
11
+ }
@@ -7,6 +7,7 @@ export * from "./csrTypes.d.ts";
7
7
  export * from "./decorators.d.ts";
8
8
  export * from "./device.d.ts";
9
9
  export * from "./frameConfig.d.ts";
10
+ export * from "./href.d.ts";
10
11
  export * from "./locale.d.ts";
11
12
  export * from "./makePageProto.d.ts";
12
13
  export * from "./mcpView.d.ts";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { getEnv } from "akanjs/base";
3
- import { cn, csrLocation, getPathInfo, router, usePage, usePathCtx } from "akanjs/client";
3
+ import { cn, csrLocation, getPathInfo, Href, router, usePage, usePathCtx } from "akanjs/client";
4
4
  import { openExternalUrl } from "akanjs/client/native";
5
5
  import { Logger } from "akanjs/common";
6
6
  import { st } from "akanjs/store";
@@ -50,10 +50,12 @@ export const CsrLink = ({
50
50
  onClick={(event) => {
51
51
  props.onClick?.(event);
52
52
  if (event.defaultPrevented) return;
53
- const isExternal = href.startsWith("http") || href.startsWith("mailto:") || href.startsWith("tel:");
53
+ const isExternal = Href.isExternal(href);
54
54
  const url = href.startsWith("#") ? `${csrLocation.pathname}#${hash}` : href;
55
- if (isExternal) void openExternalUrl(href).catch(() => window.open(href, "_blank", "noopener,noreferrer"));
56
- else if (replace) router.replace(url, { scrollToTop });
55
+ if (isExternal) {
56
+ const target = Href.toExternalUrl(href, window.location.href);
57
+ void openExternalUrl(target).catch(() => window.open(target, "_blank", "noopener,noreferrer"));
58
+ } else if (replace) router.replace(url, { scrollToTop });
57
59
  else router.push(url, { scrollToTop });
58
60
  }}
59
61
  >
@@ -78,7 +80,7 @@ export const SsrLink = ({
78
80
  const { lang } = usePage();
79
81
  const pathname = useCurrentPathname();
80
82
  const currentPath = getPathInfo(pathname, lang, prefix).path;
81
- const isExternal = href.startsWith("http") || href.startsWith("mailto:") || href.startsWith("tel:");
83
+ const isExternal = Href.isExternal(href);
82
84
  const internalPathInfo = getPathInfo(href, lang, prefix);
83
85
  const requestHref = getEnv().operationMode === "local" ? internalPathInfo.href : getPathInfo(href, lang, "").href;
84
86
  const path = internalPathInfo.path;