@h3ravel/url
Version:
Request-aware URI builder and URL manipulation utilities for H3ravel.
433 lines (427 loc) • 12.9 kB
JavaScript
//#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;