Преглед на файлове

✨ feat(utils): add hono client implementation

- add hono client core implementation in mini/src/utils/hono-client
- implement client request handling with support for query, form, json parameters
- add utility functions for URL manipulation and cookie handling
- add type definitions for hono client

♻️ refactor(utils): remove global Headers and Response registration

- comment out global Headers registration in headers-polyfill.js
- comment out global Response registration in response-polyfill.ts

🔧 chore(rpc-client): update hc import path

- change hc import from 'hono/client' to local './hono-client/client'
yourname преди 3 месеца
родител
ревизия
9097597bb7

+ 4 - 4
mini/src/utils/headers-polyfill.js

@@ -71,9 +71,9 @@ class Headers {
     }
   }
   
-  // 全局注册(如果需要)
-  if (typeof globalThis !== 'undefined' && typeof globalThis.Headers === 'undefined') {
-    globalThis.Headers = Headers;
-  }
+  // // 全局注册(如果需要)
+  // if (typeof globalThis !== 'undefined' && typeof globalThis.Headers === 'undefined') {
+  //   globalThis.Headers = Headers;
+  // }
   
   export default Headers;

+ 3 - 0
mini/src/utils/hono-client/client/client.d.ts

@@ -0,0 +1,3 @@
+// Re-export types from official hono/client
+export type { ClientRequestOptions, InferResponseType, InferRequestType, Fetch } from 'hono/client';
+export { hc } from 'hono/client';

+ 170 - 0
mini/src/utils/hono-client/client/client.js

@@ -0,0 +1,170 @@
+// src/client/client.ts
+import { serialize } from "../utils/cookie.js";
+import {
+  buildSearchParams,
+  deepMerge,
+  mergePath,
+  removeIndexString,
+  replaceUrlParam,
+  replaceUrlProtocol
+} from "./utils.js";
+import Headers from "../../headers-polyfill.js";
+var createProxy = (callback, path) => {
+  const proxy = new Proxy(() => {
+  }, {
+    get(_obj, key) {
+      if (typeof key !== "string" || key === "then") {
+        return void 0;
+      }
+      return createProxy(callback, [...path, key]);
+    },
+    apply(_1, _2, args) {
+      return callback({
+        path,
+        args
+      });
+    }
+  });
+  return proxy;
+};
+var ClientRequestImpl = class {
+  url;
+  method;
+  queryParams = void 0;
+  pathParams = {};
+  rBody;
+  cType = void 0;
+  constructor(url, method) {
+    this.url = url;
+    this.method = method;
+  }
+  fetch = async (args, opt) => {
+    if (args) {
+      if (args.query) {
+        this.queryParams = buildSearchParams(args.query);
+      }
+      if (args.form) {
+        const form = new FormData();
+        for (const [k, v] of Object.entries(args.form)) {
+          if (Array.isArray(v)) {
+            for (const v2 of v) {
+              form.append(k, v2);
+            }
+          } else {
+            form.append(k, v);
+          }
+        }
+        this.rBody = form;
+      }
+      if (args.json) {
+        this.rBody = JSON.stringify(args.json);
+        this.cType = "application/json";
+      }
+      if (args.param) {
+        this.pathParams = args.param;
+      }
+    }
+    let methodUpperCase = this.method.toUpperCase();
+    const headerValues = {
+      ...args?.header,
+      ...typeof opt?.headers === "function" ? await opt.headers() : opt?.headers
+    };
+    if (args?.cookie) {
+      const cookies = [];
+      for (const [key, value] of Object.entries(args.cookie)) {
+        cookies.push(serialize(key, value, { path: "/" }));
+      }
+      headerValues["Cookie"] = cookies.join(",");
+    }
+    if (this.cType) {
+      headerValues["Content-Type"] = this.cType;
+    }
+    const headers = new Headers(headerValues ?? void 0);
+    let url = this.url;
+    url = removeIndexString(url);
+    url = replaceUrlParam(url, this.pathParams);
+    if (this.queryParams) {
+      url = url + "?" + this.queryParams.toString();
+    }
+    methodUpperCase = this.method.toUpperCase();
+    const setBody = !(methodUpperCase === "GET" || methodUpperCase === "HEAD");
+    return (opt?.fetch || fetch)(url, {
+      body: setBody ? this.rBody : void 0,
+      method: methodUpperCase,
+      headers,
+      ...opt?.init
+    });
+  };
+};
+var hc = (baseUrl, options) => createProxy(function proxyCallback(opts) {
+  const parts = [...opts.path];
+  const lastParts = parts.slice(-3).reverse();
+  if (lastParts[0] === "toString") {
+    if (lastParts[1] === "name") {
+      return lastParts[2] || "";
+    }
+    return proxyCallback.toString();
+  }
+  if (lastParts[0] === "valueOf") {
+    if (lastParts[1] === "name") {
+      return lastParts[2] || "";
+    }
+    return proxyCallback;
+  }
+  let method = "";
+  if (/^\$/.test(lastParts[0])) {
+    const last = parts.pop();
+    if (last) {
+      method = last.replace(/^\$/, "");
+    }
+  }
+  const path = parts.join("/");
+  const url = mergePath(baseUrl, path);
+  if (method === "url") {
+    let result = url;
+    if (opts.args[0]) {
+      if (opts.args[0].param) {
+        result = replaceUrlParam(url, opts.args[0].param);
+      }
+      if (opts.args[0].query) {
+        result = result + "?" + buildSearchParams(opts.args[0].query).toString();
+      }
+    }
+    result = removeIndexString(result);
+    return new URL(result);
+  }
+  if (method === "ws") {
+    const webSocketUrl = replaceUrlProtocol(
+      opts.args[0] && opts.args[0].param ? replaceUrlParam(url, opts.args[0].param) : url,
+      "ws"
+    );
+    const targetUrl = new URL(webSocketUrl);
+    const queryParams = opts.args[0]?.query;
+    if (queryParams) {
+      Object.entries(queryParams).forEach(([key, value]) => {
+        if (Array.isArray(value)) {
+          value.forEach((item) => targetUrl.searchParams.append(key, item));
+        } else {
+          targetUrl.searchParams.set(key, value);
+        }
+      });
+    }
+    const establishWebSocket = (...args) => {
+      if (options?.webSocket !== void 0 && typeof options.webSocket === "function") {
+        return options.webSocket(...args);
+      }
+      return new WebSocket(...args);
+    };
+    return establishWebSocket(targetUrl.toString());
+  }
+  const req = new ClientRequestImpl(url, method);
+  if (method) {
+    options ??= {};
+    const args = deepMerge(options, { ...opts.args[1] });
+    return req.fetch(opts.args[0], args);
+  }
+  return req;
+}, []);
+export {
+  hc
+};

+ 5 - 0
mini/src/utils/hono-client/client/index.js

@@ -0,0 +1,5 @@
+// src/client/index.ts
+import { hc } from "./client.js";
+export {
+  hc
+};

+ 70 - 0
mini/src/utils/hono-client/client/utils.js

@@ -0,0 +1,70 @@
+// src/client/utils.ts
+var mergePath = (base, path) => {
+  base = base.replace(/\/+$/, "");
+  base = base + "/";
+  path = path.replace(/^\/+/, "");
+  return base + path;
+};
+var replaceUrlParam = (urlString, params) => {
+  for (const [k, v] of Object.entries(params)) {
+    const reg = new RegExp("/:" + k + "(?:{[^/]+})?\\??");
+    urlString = urlString.replace(reg, v ? `/${v}` : "");
+  }
+  return urlString;
+};
+var buildSearchParams = (query) => {
+  const searchParams = new URLSearchParams();
+  for (const [k, v] of Object.entries(query)) {
+    if (v === void 0) {
+      continue;
+    }
+    if (Array.isArray(v)) {
+      for (const v2 of v) {
+        searchParams.append(k, v2);
+      }
+    } else {
+      searchParams.set(k, v);
+    }
+  }
+  return searchParams;
+};
+var replaceUrlProtocol = (urlString, protocol) => {
+  switch (protocol) {
+    case "ws":
+      return urlString.replace(/^http/, "ws");
+    case "http":
+      return urlString.replace(/^ws/, "http");
+  }
+};
+var removeIndexString = (urlSting) => {
+  if (/^https?:\/\/[^\/]+?\/index$/.test(urlSting)) {
+    return urlSting.replace(/\/index$/, "/");
+  }
+  return urlSting.replace(/\/index$/, "");
+};
+function isObject(item) {
+  return typeof item === "object" && item !== null && !Array.isArray(item);
+}
+function deepMerge(target, source) {
+  if (!isObject(target) && !isObject(source)) {
+    return source;
+  }
+  const merged = { ...target };
+  for (const key in source) {
+    const value = source[key];
+    if (isObject(merged[key]) && isObject(value)) {
+      merged[key] = deepMerge(merged[key], value);
+    } else {
+      merged[key] = value;
+    }
+  }
+  return merged;
+}
+export {
+  buildSearchParams,
+  deepMerge,
+  mergePath,
+  removeIndexString,
+  replaceUrlParam,
+  replaceUrlProtocol
+};

+ 147 - 0
mini/src/utils/hono-client/utils/cookie.js

@@ -0,0 +1,147 @@
+// src/utils/cookie.ts
+import { decodeURIComponent_, tryDecode } from "./url.js";
+var algorithm = { name: "HMAC", hash: "SHA-256" };
+var getCryptoKey = async (secret) => {
+  const secretBuf = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
+  return await crypto.subtle.importKey("raw", secretBuf, algorithm, false, ["sign", "verify"]);
+};
+var makeSignature = async (value, secret) => {
+  const key = await getCryptoKey(secret);
+  const signature = await crypto.subtle.sign(algorithm.name, key, new TextEncoder().encode(value));
+  return btoa(String.fromCharCode(...new Uint8Array(signature)));
+};
+var verifySignature = async (base64Signature, value, secret) => {
+  try {
+    const signatureBinStr = atob(base64Signature);
+    const signature = new Uint8Array(signatureBinStr.length);
+    for (let i = 0, len = signatureBinStr.length; i < len; i++) {
+      signature[i] = signatureBinStr.charCodeAt(i);
+    }
+    return await crypto.subtle.verify(algorithm, secret, signature, new TextEncoder().encode(value));
+  } catch {
+    return false;
+  }
+};
+var validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/;
+var validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/;
+var parse = (cookie, name) => {
+  if (name && cookie.indexOf(name) === -1) {
+    return {};
+  }
+  const pairs = cookie.trim().split(";");
+  const parsedCookie = {};
+  for (let pairStr of pairs) {
+    pairStr = pairStr.trim();
+    const valueStartPos = pairStr.indexOf("=");
+    if (valueStartPos === -1) {
+      continue;
+    }
+    const cookieName = pairStr.substring(0, valueStartPos).trim();
+    if (name && name !== cookieName || !validCookieNameRegEx.test(cookieName)) {
+      continue;
+    }
+    let cookieValue = pairStr.substring(valueStartPos + 1).trim();
+    if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) {
+      cookieValue = cookieValue.slice(1, -1);
+    }
+    if (validCookieValueRegEx.test(cookieValue)) {
+      parsedCookie[cookieName] = cookieValue.indexOf("%") !== -1 ? tryDecode(cookieValue, decodeURIComponent_) : cookieValue;
+      if (name) {
+        break;
+      }
+    }
+  }
+  return parsedCookie;
+};
+var parseSigned = async (cookie, secret, name) => {
+  const parsedCookie = {};
+  const secretKey = await getCryptoKey(secret);
+  for (const [key, value] of Object.entries(parse(cookie, name))) {
+    const signatureStartPos = value.lastIndexOf(".");
+    if (signatureStartPos < 1) {
+      continue;
+    }
+    const signedValue = value.substring(0, signatureStartPos);
+    const signature = value.substring(signatureStartPos + 1);
+    if (signature.length !== 44 || !signature.endsWith("=")) {
+      continue;
+    }
+    const isVerified = await verifySignature(signature, signedValue, secretKey);
+    parsedCookie[key] = isVerified ? signedValue : false;
+  }
+  return parsedCookie;
+};
+var _serialize = (name, value, opt = {}) => {
+  let cookie = `${name}=${value}`;
+  if (name.startsWith("__Secure-") && !opt.secure) {
+    throw new Error("__Secure- Cookie must have Secure attributes");
+  }
+  if (name.startsWith("__Host-")) {
+    if (!opt.secure) {
+      throw new Error("__Host- Cookie must have Secure attributes");
+    }
+    if (opt.path !== "/") {
+      throw new Error('__Host- Cookie must have Path attributes with "/"');
+    }
+    if (opt.domain) {
+      throw new Error("__Host- Cookie must not have Domain attributes");
+    }
+  }
+  if (opt && typeof opt.maxAge === "number" && opt.maxAge >= 0) {
+    if (opt.maxAge > 3456e4) {
+      throw new Error(
+        "Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration."
+      );
+    }
+    cookie += `; Max-Age=${opt.maxAge | 0}`;
+  }
+  if (opt.domain && opt.prefix !== "host") {
+    cookie += `; Domain=${opt.domain}`;
+  }
+  if (opt.path) {
+    cookie += `; Path=${opt.path}`;
+  }
+  if (opt.expires) {
+    if (opt.expires.getTime() - Date.now() > 3456e7) {
+      throw new Error(
+        "Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future."
+      );
+    }
+    cookie += `; Expires=${opt.expires.toUTCString()}`;
+  }
+  if (opt.httpOnly) {
+    cookie += "; HttpOnly";
+  }
+  if (opt.secure) {
+    cookie += "; Secure";
+  }
+  if (opt.sameSite) {
+    cookie += `; SameSite=${opt.sameSite.charAt(0).toUpperCase() + opt.sameSite.slice(1)}`;
+  }
+  if (opt.priority) {
+    cookie += `; Priority=${opt.priority}`;
+  }
+  if (opt.partitioned) {
+    if (!opt.secure) {
+      throw new Error("Partitioned Cookie must have Secure attributes");
+    }
+    cookie += "; Partitioned";
+  }
+  return cookie;
+};
+var serialize = (name, value, opt) => {
+  value = encodeURIComponent(value);
+  return _serialize(name, value, opt);
+};
+var serializeSigned = async (name, value, secret, opt = {}) => {
+  const signature = await makeSignature(value, secret);
+  value = `${value}.${signature}`;
+  value = encodeURIComponent(value);
+  return _serialize(name, value, opt);
+};
+export {
+  parse,
+  parseSigned,
+  serialize,
+  serializeSigned
+};

+ 219 - 0
mini/src/utils/hono-client/utils/url.js

@@ -0,0 +1,219 @@
+// src/utils/url.ts
+var splitPath = (path) => {
+  const paths = path.split("/");
+  if (paths[0] === "") {
+    paths.shift();
+  }
+  return paths;
+};
+var splitRoutingPath = (routePath) => {
+  const { groups, path } = extractGroupsFromPath(routePath);
+  const paths = splitPath(path);
+  return replaceGroupMarks(paths, groups);
+};
+var extractGroupsFromPath = (path) => {
+  const groups = [];
+  path = path.replace(/\{[^}]+\}/g, (match, index) => {
+    const mark = `@${index}`;
+    groups.push([mark, match]);
+    return mark;
+  });
+  return { groups, path };
+};
+var replaceGroupMarks = (paths, groups) => {
+  for (let i = groups.length - 1; i >= 0; i--) {
+    const [mark] = groups[i];
+    for (let j = paths.length - 1; j >= 0; j--) {
+      if (paths[j].includes(mark)) {
+        paths[j] = paths[j].replace(mark, groups[i][1]);
+        break;
+      }
+    }
+  }
+  return paths;
+};
+var patternCache = {};
+var getPattern = (label, next) => {
+  if (label === "*") {
+    return "*";
+  }
+  const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
+  if (match) {
+    const cacheKey = `${label}#${next}`;
+    if (!patternCache[cacheKey]) {
+      if (match[2]) {
+        patternCache[cacheKey] = next && next[0] !== ":" && next[0] !== "*" ? [cacheKey, match[1], new RegExp(`^${match[2]}(?=/${next})`)] : [label, match[1], new RegExp(`^${match[2]}$`)];
+      } else {
+        patternCache[cacheKey] = [label, match[1], true];
+      }
+    }
+    return patternCache[cacheKey];
+  }
+  return null;
+};
+var tryDecode = (str, decoder) => {
+  try {
+    return decoder(str);
+  } catch {
+    return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
+      try {
+        return decoder(match);
+      } catch {
+        return match;
+      }
+    });
+  }
+};
+var tryDecodeURI = (str) => tryDecode(str, decodeURI);
+var getPath = (request) => {
+  const url = request.url;
+  const start = url.indexOf(
+    "/",
+    url.charCodeAt(9) === 58 ? 13 : 8
+  );
+  let i = start;
+  for (; i < url.length; i++) {
+    const charCode = url.charCodeAt(i);
+    if (charCode === 37) {
+      const queryIndex = url.indexOf("?", i);
+      const path = url.slice(start, queryIndex === -1 ? void 0 : queryIndex);
+      return tryDecodeURI(path.includes("%25") ? path.replace(/%25/g, "%2525") : path);
+    } else if (charCode === 63) {
+      break;
+    }
+  }
+  return url.slice(start, i);
+};
+var getQueryStrings = (url) => {
+  const queryIndex = url.indexOf("?", 8);
+  return queryIndex === -1 ? "" : "?" + url.slice(queryIndex + 1);
+};
+var getPathNoStrict = (request) => {
+  const result = getPath(request);
+  return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result;
+};
+var mergePath = (base, sub, ...rest) => {
+  if (rest.length) {
+    sub = mergePath(sub, ...rest);
+  }
+  return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
+};
+var checkOptionalParameter = (path) => {
+  if (path.charCodeAt(path.length - 1) !== 63 || !path.includes(":")) {
+    return null;
+  }
+  const segments = path.split("/");
+  const results = [];
+  let basePath = "";
+  segments.forEach((segment) => {
+    if (segment !== "" && !/\:/.test(segment)) {
+      basePath += "/" + segment;
+    } else if (/\:/.test(segment)) {
+      if (/\?/.test(segment)) {
+        if (results.length === 0 && basePath === "") {
+          results.push("/");
+        } else {
+          results.push(basePath);
+        }
+        const optionalSegment = segment.replace("?", "");
+        basePath += "/" + optionalSegment;
+        results.push(basePath);
+      } else {
+        basePath += "/" + segment;
+      }
+    }
+  });
+  return results.filter((v, i, a) => a.indexOf(v) === i);
+};
+var _decodeURI = (value) => {
+  if (!/[%+]/.test(value)) {
+    return value;
+  }
+  if (value.indexOf("+") !== -1) {
+    value = value.replace(/\+/g, " ");
+  }
+  return value.indexOf("%") !== -1 ? tryDecode(value, decodeURIComponent_) : value;
+};
+var _getQueryParam = (url, key, multiple) => {
+  let encoded;
+  if (!multiple && key && !/[%+]/.test(key)) {
+    let keyIndex2 = url.indexOf(`?${key}`, 8);
+    if (keyIndex2 === -1) {
+      keyIndex2 = url.indexOf(`&${key}`, 8);
+    }
+    while (keyIndex2 !== -1) {
+      const trailingKeyCode = url.charCodeAt(keyIndex2 + key.length + 1);
+      if (trailingKeyCode === 61) {
+        const valueIndex = keyIndex2 + key.length + 2;
+        const endIndex = url.indexOf("&", valueIndex);
+        return _decodeURI(url.slice(valueIndex, endIndex === -1 ? void 0 : endIndex));
+      } else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) {
+        return "";
+      }
+      keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
+    }
+    encoded = /[%+]/.test(url);
+    if (!encoded) {
+      return void 0;
+    }
+  }
+  const results = {};
+  encoded ??= /[%+]/.test(url);
+  let keyIndex = url.indexOf("?", 8);
+  while (keyIndex !== -1) {
+    const nextKeyIndex = url.indexOf("&", keyIndex + 1);
+    let valueIndex = url.indexOf("=", keyIndex);
+    if (valueIndex > nextKeyIndex && nextKeyIndex !== -1) {
+      valueIndex = -1;
+    }
+    let name = url.slice(
+      keyIndex + 1,
+      valueIndex === -1 ? nextKeyIndex === -1 ? void 0 : nextKeyIndex : valueIndex
+    );
+    if (encoded) {
+      name = _decodeURI(name);
+    }
+    keyIndex = nextKeyIndex;
+    if (name === "") {
+      continue;
+    }
+    let value;
+    if (valueIndex === -1) {
+      value = "";
+    } else {
+      value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? void 0 : nextKeyIndex);
+      if (encoded) {
+        value = _decodeURI(value);
+      }
+    }
+    if (multiple) {
+      if (!(results[name] && Array.isArray(results[name]))) {
+        results[name] = [];
+      }
+      ;
+      results[name].push(value);
+    } else {
+      results[name] ??= value;
+    }
+  }
+  return key ? results[key] : results;
+};
+var getQueryParam = _getQueryParam;
+var getQueryParams = (url, key) => {
+  return _getQueryParam(url, key, true);
+};
+var decodeURIComponent_ = decodeURIComponent;
+export {
+  checkOptionalParameter,
+  decodeURIComponent_,
+  getPath,
+  getPathNoStrict,
+  getPattern,
+  getQueryParam,
+  getQueryParams,
+  getQueryStrings,
+  mergePath,
+  splitPath,
+  splitRoutingPath,
+  tryDecode
+};

+ 4 - 4
mini/src/utils/response-polyfill.ts

@@ -80,9 +80,9 @@ class ResponsePolyfill {
     }
   }
   
-  // 全局注册(如果需要)
-  if (typeof globalThis !== 'undefined' && typeof globalThis.Response === 'undefined') {
-    globalThis.Response = ResponsePolyfill as any
-  }
+  // // 全局注册(如果需要)
+  // if (typeof globalThis !== 'undefined' && typeof globalThis.Response === 'undefined') {
+  //   globalThis.Response = ResponsePolyfill as any
+  // }
   
   export default ResponsePolyfill

+ 1 - 1
mini/src/utils/rpc-client.ts

@@ -1,6 +1,6 @@
 import Taro from '@tarojs/taro'
-import { hc } from 'hono/client'
 import ResponsePolyfill from './response-polyfill'
+import { hc } from './hono-client/client'
 
 // API配置
 const API_BASE_URL = process.env.TARO_APP_API_BASE_URL || 'http://localhost:8080'