UNPKG

@h3ravel/url

Version:

Request-aware URI builder and URL manipulation utilities for H3ravel.

433 lines (427 loc) 12.9 kB
//#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to$1, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to$1, key) && key !== except) __defProp(to$1, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to$1; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion let __h3ravel_core = require("@h3ravel/core"); let __h3ravel_support = require("@h3ravel/support"); let node_path = require("node:path"); node_path = __toESM(node_path); //#region src/RequestAwareHelpers.ts /** * Request-aware URL helper class */ var RequestAwareHelpers = class { baseUrl = ""; constructor(app) { this.app = app; try { this.baseUrl = config("app.url", "http://localhost:3000"); } catch {} } /** * Get the current request instance */ getCurrentRequest() { const request = this.app.make("http.request"); if (!request) throw new Error("Request instance not available in current context"); return request; } /** * Get the current request URL (path only, no query string) */ current() { const raw = this.getCurrentRequest().getEvent().req.url ?? "/"; return new URL(raw, "http://localhost").pathname; } /** * Get the full current URL with query string */ full() { const requestUrl = this.getCurrentRequest().getEvent().req.url ?? "/"; if (requestUrl.startsWith("http")) return requestUrl; return new URL(requestUrl, this.baseUrl).toString(); } /** * Get the previous request URL from session or referrer */ previous() { const headers = this.getCurrentRequest().getEvent()?.node?.req?.headers; let referrer = headers?.referer ?? headers?.referrer; if (Array.isArray(referrer)) referrer = referrer[0]; if (referrer) return referrer; return this.current(); } /** * Get the previous request path (without query string) */ previousPath() { const previousUrl = this.previous(); try { return new URL(previousUrl).pathname; } catch { return previousUrl; } } /** * Get the current query parameters */ query() { return this.getCurrentRequest().query || {}; } }; /** * Global helper function factory */ function createUrlHelper(app) { return () => new RequestAwareHelpers(app); } //#endregion //#region src/Url.ts /** * URL builder class with fluent API and request-aware helpers */ var Url = class Url { _scheme; _host; _port; _path; _query; _fragment; app; constructor(app, scheme, host, port, path$1 = "/", query = {}, fragment) { this.app = app; this._scheme = scheme; this._host = host; this._port = port; this._path = path$1.startsWith("/") ? path$1 : `/${path$1}`; this._query = { ...query }; this._fragment = fragment; } /** * Create a URL from a full URL string */ static of(url$1, app) { try { const parsed = new URL(url$1); const query = {}; parsed.searchParams.forEach((value, key) => { query[key] = value; }); return new Url(app, parsed.protocol.replace(":", ""), parsed.hostname, parsed.port ? parseInt(parsed.port) : void 0, parsed.pathname || "/", query, parsed.hash ? parsed.hash.substring(1) : void 0); } catch { throw new Error(`Invalid URL: ${url$1}`); } } /** * Create a URL from a path relative to the app URL */ static to(path$1, app) { let baseUrl = ""; try { baseUrl = config("app.url", "http://localhost:3000"); } catch {} const fullUrl = new URL(path$1, baseUrl).toString(); return Url.of(fullUrl, app); } /** * Create a URL from a named route */ static route(name, params = {}, app) { if (!app) throw new Error("Application instance required for route generation"); const router = app.make("router"); if (!router || typeof router.route !== "function") throw new Error("Router not available or does not support route generation"); if (typeof router.route !== "function") throw new Error("Router does not support route generation"); const routeUrl = router.route(name, params); if (!routeUrl) throw new Error(`Route "${name}" not found`); return Url.to(routeUrl, app); } /** * Create a signed URL from a named route */ static signedRoute(name, params = {}, app) { return Url.route(name, params, app).withSignature(app); } /** * Create a temporary signed URL from a named route */ static temporarySignedRoute(name, params = {}, expiration, app) { return Url.route(name, params, app).withSignature(app, expiration); } /** * Create a URL from a controller action */ static action(controller, params, app) { if (!app) throw new Error("Application instance required for action URL generation"); const [controllerName, methodName = "index"] = typeof controller === "string" ? controller.split("@") : controller; const cname = typeof controllerName === "string" ? controllerName : controllerName.name; const routes = app.make("app.routes"); if (!Array.isArray(routes)) throw new Error("Action URL generation requires router integration - not yet implemented"); if (routes.length < 1) throw new Error(`No routes available to resolve action: ${controller}`); const found = routes.find((route$1) => { return route$1.signature?.[0] === cname && (route$1.signature?.[1] || "index") === methodName; }); if (!found) throw new Error(`No route found for ${cname}`); const _params = Object.values(params ?? {}).join("/"); if (_params) return Url.to(node_path.default.join(found.path, _params)); return Url.to(found.path, app); } /** * Set the scheme (protocol) of the URL */ withScheme(scheme) { return new Url(this.app, scheme, this._host, this._port, this._path, this._query, this._fragment); } /** * Set the host of the URL */ withHost(host) { return new Url(this.app, this._scheme, host, this._port, this._path, this._query, this._fragment); } /** * Set the port of the URL */ withPort(port) { return new Url(this.app, this._scheme, this._host, port, this._path, this._query, this._fragment); } /** * Set the path of the URL */ withPath(path$1) { return new Url(this.app, this._scheme, this._host, this._port, path$1, this._query, this._fragment); } /** * Set the query parameters of the URL */ withQuery(query) { return new Url(this.app, this._scheme, this._host, this._port, this._path, { ...query }, this._fragment); } /** * Merge additional query parameters */ withQueryParams(params) { return new Url(this.app, this._scheme, this._host, this._port, this._path, { ...this._query, ...params }, this._fragment); } /** * Set the fragment (hash) of the URL */ withFragment(fragment) { return new Url(this.app, this._scheme, this._host, this._port, this._path, this._query, fragment); } /** * Add a signature to the URL for security */ withSignature(app, expiration) { if (!(app || this.app)) throw new Error("Application instance required for URL signing"); let key = ""; try { key = config("app.key"); } catch {} if (!key) throw new __h3ravel_core.ConfigException("APP_KEY and app.key", "any", this); const url$1 = this.toString(); const queryParams = { ...this._query }; if (expiration) queryParams.expires = Math.floor(expiration / 1e3); queryParams.signature = (0, __h3ravel_support.hmac)(expiration ? `${url$1}?expires=${queryParams.expires}` : url$1, key); return this.withQuery(queryParams); } /** * Verify if a URL signature is valid */ hasValidSignature(app) { if (!(app || this.app)) return false; const signature = this._query.signature; if (!signature) return false; if (this._query.expires !== void 0 && this._query.expires !== null) { const expiresStr = String(this._query.expires); const expirationTime = parseInt(expiresStr, 10) * 1e3; if (isNaN(expirationTime) || Date.now() > expirationTime) return false; } const queryWithoutSignature = { ...this._query }; delete queryWithoutSignature.signature; const urlWithoutSignature = new Url(this.app, this._scheme, this._host, this._port, this._path, queryWithoutSignature, this._fragment).toString(); const payload = this._query.expires ? `${urlWithoutSignature}?expires=${this._query.expires}` : urlWithoutSignature; let key = ""; try { key = config("app.key", "default-key"); } catch {} return signature === (0, __h3ravel_support.hmac)(payload, key); } /** * Convert the URL to its string representation */ toString() { let url$1 = ""; if (this._scheme && this._host) { url$1 += `${this._scheme}://${this._host}`; if (this._port && !(this._scheme === "http" && this._port === 80 || this._scheme === "https" && this._port === 443)) url$1 += `:${this._port}`; } if (this._path) { if (!this._path.startsWith("/")) url$1 += "/"; url$1 += this._path; } const queryEntries = Object.entries(this._query); if (queryEntries.length > 0) { const queryString = queryEntries.map(([key, value]) => { if (Array.isArray(value)) return value.map((v) => `${encodeURIComponent(key)}%5B%5D=${encodeURIComponent(v)}`).join("&"); return `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`; }).join("&"); url$1 += `?${queryString}`; } if (this._fragment) url$1 += `#${this._fragment}`; return url$1; } /** * Get the scheme */ getScheme() { return this._scheme; } /** * Get the host */ getHost() { return this._host; } /** * Get the port */ getPort() { return this._port; } /** * Get the path */ getPath() { return this._path; } /** * Get the query parameters */ getQuery() { return { ...this._query }; } /** * Get the fragment */ getFragment() { return this._fragment; } }; //#endregion //#region src/Helpers.ts /** * Global helper functions for URL manipulation */ /** * Create a URL from a path relative to the app URL */ function to(path$1, app) { return Url.to(path$1, app); } /** * Create a URL from a named route */ function route(name, params = {}, app) { return Url.route(name, params, app); } /** * Create a signed URL from a named route */ function signedRoute(name, params = {}, app) { return Url.signedRoute(name, params, app); } /** * Create a temporary signed URL from a named route */ function temporarySignedRoute(name, params = {}, expiration, app) { return Url.temporarySignedRoute(name, params, expiration, app); } /** * Create a URL from a controller action */ function action(controller, app) { return Url.action(controller, app); } /** * Get request-aware URL helpers */ function url(app) { if (!app) throw new Error("Application instance required for request-aware URL helpers"); return new RequestAwareHelpers(app); } /** * Create URL helpers that are bound to an application instance */ function createUrlHelpers(app) { return { to: (path$1) => Url.to(path$1, app), route: (name, params = {}) => Url.route(name, params, app).toString(), signedRoute: (name, params = {}) => Url.signedRoute(name, params, app), temporarySignedRoute: (name, params = {}, expiration) => Url.temporarySignedRoute(name, params, expiration, app), action: (controller, params) => Url.action(controller, params, app).toString(), url: (path$1) => { if (path$1) return Url.to(path$1).toString(); return new RequestAwareHelpers(app); } }; } //#endregion //#region src/Providers/UrlServiceProvider.ts /** * Service provider for URL utilities */ var UrlServiceProvider = class extends __h3ravel_core.ServiceProvider { static priority = 897; /** * Register URL services in the container */ register() {} /** * Boot URL services */ boot() { this.app.singleton("app.url", () => Url); this.app.singleton("app.url.helper", () => createUrlHelper(this.app)); this.app.singleton("app.url.helpers", () => createUrlHelpers(this.app)); if (typeof globalThis !== "undefined") { const helpers = createUrlHelpers(this.app); Object.assign(globalThis, { url: helpers.url, route: helpers.route, action: helpers.action }); } } }; //#endregion exports.RequestAwareHelpers = RequestAwareHelpers; exports.Url = Url; exports.UrlServiceProvider = UrlServiceProvider; exports.action = action; exports.createUrlHelper = createUrlHelper; exports.createUrlHelpers = createUrlHelpers; exports.route = route; exports.signedRoute = signedRoute; exports.temporarySignedRoute = temporarySignedRoute; exports.to = to; exports.url = url;