blade
Version:
React at the edge.
924 lines (906 loc) • 31.2 kB
JavaScript
import { r as __toESM, t as __commonJS } from "./chunk-DUEDWNxO.js";
import { a as populatePathSegments, c as DEFAULT_COOKIE_MAX_AGE, n as getCookieSetter, r as getOutputFile, s as CUSTOM_HEADERS } from "./utils-BIjILkWh.js";
import { t as RootServerContext } from "./context-BBeH3CVO.js";
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
import { hydrateRoot } from "react-dom/client";
//#region private/client/context.ts
const RootClientContext = createContext(null);
//#endregion
//#region ../../node_modules/radash/dist/esm/typed.mjs
const isArray = Array.isArray;
const isObject = (value) => {
return !!value && value.constructor === Object;
};
const isPrimitive = (value) => {
return value === void 0 || value === null || typeof value !== "object" && typeof value !== "function";
};
//#endregion
//#region ../../node_modules/radash/dist/esm/number.mjs
const toInt = (value, defaultValue) => {
const def = defaultValue === void 0 ? 0 : defaultValue;
if (value === null || value === void 0) return def;
const result = parseInt(value);
return isNaN(result) ? def : result;
};
//#endregion
//#region ../../node_modules/radash/dist/esm/object.mjs
const clone = (obj) => {
if (isPrimitive(obj)) return obj;
if (typeof obj === "function") return obj.bind({});
const newObj = new obj.constructor();
Object.getOwnPropertyNames(obj).forEach((prop) => {
newObj[prop] = obj[prop];
});
return newObj;
};
const omit = (obj, keys2) => {
if (!obj) return {};
if (!keys2 || keys2.length === 0) return obj;
return keys2.reduce((acc, key) => {
delete acc[key];
return acc;
}, { ...obj });
};
const set = (initial, path, value) => {
if (!initial) return {};
if (!path || value === void 0) return initial;
const segments = path.split(/[\.\[\]]/g).filter((x) => !!x.trim());
const _set = (node) => {
if (segments.length > 1) {
const key = segments.shift();
const nextIsNum = toInt(segments[0], null) === null ? false : true;
node[key] = node[key] === void 0 ? nextIsNum ? [] : {} : node[key];
_set(node[key]);
} else node[segments[0]] = value;
};
const cloned = clone(initial);
_set(cloned);
return cloned;
};
const assign = (initial, override) => {
if (!initial || !override) return initial ?? override ?? {};
return Object.entries({
...initial,
...override
}).reduce((acc, [key, value]) => {
return {
...acc,
[key]: (() => {
if (isObject(initial[key])) return assign(initial[key], value);
return value;
})()
};
}, {});
};
const construct = (obj) => {
if (!obj) return {};
return Object.keys(obj).reduce((acc, path) => {
return set(acc, path, obj[path]);
}, {});
};
//#endregion
//#region private/client/utils/constants.ts
const IS_CLIENT_DEV = import.meta.env.BLADE_ENV === "development";
/**
* A name prefix used to identify an input whose value should be used to resolve a target
* record instead of being treated as a value to store in the record.
*
* We are using this prefix in order to enable progressive enhancement in the future,
* meaning making it possible to submit forms even before their JavaScript is downloaded.
*/
const FORM_TARGET_PREFIX = "__blade_target_";
//#endregion
//#region ../../node_modules/eventsource-parser/dist/index.js
var ParseError = class extends Error {
constructor(message, options) {
super(message), this.name = "ParseError", this.type = options.type, this.field = options.field, this.value = options.value, this.line = options.line;
}
};
function noop(_arg) {}
function createParser(callbacks) {
if (typeof callbacks == "function") throw new TypeError("`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?");
const { onEvent = noop, onError = noop, onRetry = noop, onComment } = callbacks;
let incompleteLine = "", isFirstChunk = !0, id, data = "", eventType = "";
function feed(newChunk) {
const chunk = isFirstChunk ? newChunk.replace(/^\xEF\xBB\xBF/, "") : newChunk, [complete, incomplete] = splitLines(`${incompleteLine}${chunk}`);
for (const line of complete) parseLine(line);
incompleteLine = incomplete, isFirstChunk = !1;
}
function parseLine(line) {
if (line === "") {
dispatchEvent();
return;
}
if (line.startsWith(":")) {
onComment && onComment(line.slice(line.startsWith(": ") ? 2 : 1));
return;
}
const fieldSeparatorIndex = line.indexOf(":");
if (fieldSeparatorIndex !== -1) {
const field = line.slice(0, fieldSeparatorIndex), offset = line[fieldSeparatorIndex + 1] === " " ? 2 : 1;
processField(field, line.slice(fieldSeparatorIndex + offset), line);
return;
}
processField(line, "", line);
}
function processField(field, value, line) {
switch (field) {
case "event":
eventType = value;
break;
case "data":
data = `${data}${value}
`;
break;
case "id":
id = value.includes("\0") ? void 0 : value;
break;
case "retry":
/^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(new ParseError(`Invalid \`retry\` value: "${value}"`, {
type: "invalid-retry",
value,
line
}));
break;
default:
onError(new ParseError(`Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`, {
type: "unknown-field",
field,
value,
line
}));
break;
}
}
function dispatchEvent() {
data.length > 0 && onEvent({
id,
event: eventType || void 0,
data: data.endsWith(`
`) ? data.slice(0, -1) : data
}), id = void 0, data = "", eventType = "";
}
function reset(options = {}) {
incompleteLine && options.consume && parseLine(incompleteLine), isFirstChunk = !0, id = void 0, data = "", eventType = "", incompleteLine = "";
}
return {
feed,
reset
};
}
function splitLines(chunk) {
const lines = [];
let incompleteLine = "", searchIndex = 0;
for (; searchIndex < chunk.length;) {
const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(`
`, searchIndex);
let lineEnd = -1;
if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = Math.min(crIndex, lfIndex) : crIndex !== -1 ? lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1) {
incompleteLine = chunk.slice(searchIndex);
break;
} else {
const line = chunk.slice(searchIndex, lineEnd);
lines.push(line), searchIndex = lineEnd + 1, chunk[searchIndex - 1] === "\r" && chunk[searchIndex] === `
` && searchIndex++;
}
}
return [lines, incompleteLine];
}
//#endregion
//#region ../../node_modules/retry/lib/retry_operation.js
var require_retry_operation = /* @__PURE__ */ __commonJS({ "../../node_modules/retry/lib/retry_operation.js": ((exports, module) => {
function RetryOperation$1(timeouts, options) {
if (typeof options === "boolean") options = { forever: options };
this._originalTimeouts = JSON.parse(JSON.stringify(timeouts));
this._timeouts = timeouts;
this._options = options || {};
this._maxRetryTime = options && options.maxRetryTime || Infinity;
this._fn = null;
this._errors = [];
this._attempts = 1;
this._operationTimeout = null;
this._operationTimeoutCb = null;
this._timeout = null;
this._operationStart = null;
this._timer = null;
if (this._options.forever) this._cachedTimeouts = this._timeouts.slice(0);
}
module.exports = RetryOperation$1;
RetryOperation$1.prototype.reset = function() {
this._attempts = 1;
this._timeouts = this._originalTimeouts.slice(0);
};
RetryOperation$1.prototype.stop = function() {
if (this._timeout) clearTimeout(this._timeout);
if (this._timer) clearTimeout(this._timer);
this._timeouts = [];
this._cachedTimeouts = null;
};
RetryOperation$1.prototype.retry = function(err) {
if (this._timeout) clearTimeout(this._timeout);
if (!err) return false;
var currentTime = (/* @__PURE__ */ new Date()).getTime();
if (err && currentTime - this._operationStart >= this._maxRetryTime) {
this._errors.push(err);
this._errors.unshift(/* @__PURE__ */ new Error("RetryOperation timeout occurred"));
return false;
}
this._errors.push(err);
var timeout = this._timeouts.shift();
if (timeout === void 0) if (this._cachedTimeouts) {
this._errors.splice(0, this._errors.length - 1);
timeout = this._cachedTimeouts.slice(-1);
} else return false;
var self = this;
this._timer = setTimeout(function() {
self._attempts++;
if (self._operationTimeoutCb) {
self._timeout = setTimeout(function() {
self._operationTimeoutCb(self._attempts);
}, self._operationTimeout);
if (self._options.unref) self._timeout.unref();
}
self._fn(self._attempts);
}, timeout);
if (this._options.unref) this._timer.unref();
return true;
};
RetryOperation$1.prototype.attempt = function(fn, timeoutOps) {
this._fn = fn;
if (timeoutOps) {
if (timeoutOps.timeout) this._operationTimeout = timeoutOps.timeout;
if (timeoutOps.cb) this._operationTimeoutCb = timeoutOps.cb;
}
var self = this;
if (this._operationTimeoutCb) this._timeout = setTimeout(function() {
self._operationTimeoutCb();
}, self._operationTimeout);
this._operationStart = (/* @__PURE__ */ new Date()).getTime();
this._fn(this._attempts);
};
RetryOperation$1.prototype.try = function(fn) {
console.log("Using RetryOperation.try() is deprecated");
this.attempt(fn);
};
RetryOperation$1.prototype.start = function(fn) {
console.log("Using RetryOperation.start() is deprecated");
this.attempt(fn);
};
RetryOperation$1.prototype.start = RetryOperation$1.prototype.try;
RetryOperation$1.prototype.errors = function() {
return this._errors;
};
RetryOperation$1.prototype.attempts = function() {
return this._attempts;
};
RetryOperation$1.prototype.mainError = function() {
if (this._errors.length === 0) return null;
var counts = {};
var mainError = null;
var mainErrorCount = 0;
for (var i = 0; i < this._errors.length; i++) {
var error = this._errors[i];
var message = error.message;
var count = (counts[message] || 0) + 1;
counts[message] = count;
if (count >= mainErrorCount) {
mainError = error;
mainErrorCount = count;
}
}
return mainError;
};
}) });
//#endregion
//#region ../../node_modules/retry/lib/retry.js
var require_retry$1 = /* @__PURE__ */ __commonJS({ "../../node_modules/retry/lib/retry.js": ((exports) => {
var RetryOperation = require_retry_operation();
exports.operation = function(options) {
return new RetryOperation(exports.timeouts(options), {
forever: options && (options.forever || options.retries === Infinity),
unref: options && options.unref,
maxRetryTime: options && options.maxRetryTime
});
};
exports.timeouts = function(options) {
if (options instanceof Array) return [].concat(options);
var opts = {
retries: 10,
factor: 2,
minTimeout: 1 * 1e3,
maxTimeout: Infinity,
randomize: false
};
for (var key in options) opts[key] = options[key];
if (opts.minTimeout > opts.maxTimeout) throw new Error("minTimeout is greater than maxTimeout");
var timeouts = [];
for (var i = 0; i < opts.retries; i++) timeouts.push(this.createTimeout(i, opts));
if (options && options.forever && !timeouts.length) timeouts.push(this.createTimeout(i, opts));
timeouts.sort(function(a, b) {
return a - b;
});
return timeouts;
};
exports.createTimeout = function(attempt, opts) {
var random = opts.randomize ? Math.random() + 1 : 1;
var timeout = Math.round(random * Math.max(opts.minTimeout, 1) * Math.pow(opts.factor, attempt));
timeout = Math.min(timeout, opts.maxTimeout);
return timeout;
};
exports.wrap = function(obj, options, methods) {
if (options instanceof Array) {
methods = options;
options = null;
}
if (!methods) {
methods = [];
for (var key in obj) if (typeof obj[key] === "function") methods.push(key);
}
for (var i = 0; i < methods.length; i++) {
var method = methods[i];
var original = obj[method];
obj[method] = function retryWrapper(original$1) {
var op = exports.operation(options);
var args = Array.prototype.slice.call(arguments, 1);
var callback = args.pop();
args.push(function(err) {
if (op.retry(err)) return;
if (err) arguments[0] = op.mainError();
callback.apply(this, arguments);
});
op.attempt(function() {
original$1.apply(obj, args);
});
}.bind(obj, original);
obj[method].options = options;
}
};
}) });
//#endregion
//#region ../../node_modules/retry/index.js
var require_retry = /* @__PURE__ */ __commonJS({ "../../node_modules/retry/index.js": ((exports, module) => {
module.exports = require_retry$1();
}) });
//#endregion
//#region ../../node_modules/async-retry/lib/index.js
var require_lib = /* @__PURE__ */ __commonJS({ "../../node_modules/async-retry/lib/index.js": ((exports, module) => {
var retrier = require_retry();
function retry$1(fn, opts) {
function run(resolve, reject) {
var options = opts || {};
var op;
if (!("randomize" in options)) options.randomize = true;
op = retrier.operation(options);
function bail(err) {
reject(err || /* @__PURE__ */ new Error("Aborted"));
}
function onError(err, num) {
if (err.bail) {
bail(err);
return;
}
if (!op.retry(err)) reject(op.mainError());
else if (options.onRetry) options.onRetry(err, num);
}
function runAttempt(num) {
var val;
try {
val = fn(bail, num);
} catch (err) {
onError(err, num);
return;
}
Promise.resolve(val).then(resolve).catch(function catchIt(err) {
onError(err, num);
});
}
op.attempt(runAttempt);
}
return new Promise(run);
}
module.exports = retry$1;
}) });
//#endregion
//#region private/client/utils/data.ts
var import_lib = /* @__PURE__ */ __toESM(require_lib(), 1);
const retryOptions = {
minTimeout: 10,
retries: 5,
factor: 6,
maxRetryAfter: 20
};
/**
* Like `fetch`, except that failed requests are retried automatically.
*
* @param request - A URL or `Request` object.
* @param requestInit - An optional `Request` or `RequestInit` object.
*
* @returns A native `fetch` response.
*/
const fetchRetry = async (request, requestInit) => {
return (0, import_lib.default)(async (bail) => {
try {
const response = await fetch(request, requestInit);
if (response.status >= 500 && response.status < 600 || response.status === 429) {
const retryAfter = response.headers.has("retry-after") ? Number.parseInt(response.headers.get("retry-after"), 10) : null;
if (retryAfter) {
if (retryAfter > retryOptions.maxRetryAfter) return response;
await new Promise((r) => {
setTimeout(r, retryAfter * 1e3);
});
}
const error = new Error(response.statusText);
error.body = await response.text();
error.statusCode = response.status;
throw error;
}
return response;
} catch (err) {
const error = err;
if (error.type === "aborted") return bail(error);
throw err;
}
});
};
//#endregion
//#region private/client/utils/parser.ts
const REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element");
const parseModel = (response, json) => JSON.parse(json, response._fromJSON);
const createPendingChunk = () => {
let resolve;
const promise = new Promise((r) => {
resolve = r;
});
promise.resolve = resolve;
return promise;
};
const createElement = (type, key, props) => {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
ref: null,
props,
_owner: null
};
Object.defineProperty(element, "_self", {
configurable: false,
enumerable: false,
writable: false,
value: null
});
Object.defineProperty(element, "_source", {
configurable: false,
enumerable: false,
writable: false,
value: null
});
return element;
};
const parseModelString = (response, value) => {
if (value === "$") return REACT_ELEMENT_TYPE;
if (value[0] !== "$") return value;
switch (value[1]) {
case "$": return value.substring(1);
case "S": return Symbol.for(value.substring(2));
default: {
const id = Number.parseInt(value.substring(1), 16);
const chunk = response._chunks.get(id);
if (!chunk) throw new Error(`Missing chunk with id: ${id}`);
if (chunk._result !== void 0) return chunk._result;
throw chunk;
}
}
};
const parseModelTuple = (value) => {
return Array.isArray(value) && value[0] === REACT_ELEMENT_TYPE ? createElement(value[1], value[2], value[3]) : value;
};
const resolveModel = (response, id, model) => {
const chunk = response._chunks.get(id);
if (chunk) {
const value = parseModel(response, model);
chunk._result = value;
chunk.resolve(value);
}
};
const resolveModule = (response, id, model) => {
const chunks = response._chunks;
const chunk = createPendingChunk();
const moduleReference = parseModel(response, model);
const value = window["BLADE_CHUNKS"][moduleReference.chunks[0]][moduleReference.name];
chunk._result = value;
chunk.resolve(value);
chunks.set(id, chunk);
};
const processFullRow = (response, row) => {
if (row === "") return;
const colon = row.indexOf(":", 0);
const id = Number.parseInt(row.substring(0, colon), 16);
switch (row[colon + 1]) {
case "I":
resolveModule(response, id, row.substring(colon + 2));
return;
default:
resolveModel(response, id, row.substring(colon + 1));
return;
}
};
const processBinaryChunk = (response, defaultChunk) => {
let chunk = defaultChunk;
const stringDecoder = new TextDecoder();
let linebreak = chunk.indexOf(10);
while (linebreak > -1) {
processFullRow(response, response._partialRow + stringDecoder.decode(chunk.subarray(0, linebreak)));
response._partialRow = "";
chunk = chunk.subarray(linebreak + 1);
linebreak = chunk.indexOf(10);
}
response._partialRow += stringDecoder.decode(chunk, { stream: true });
};
const createFromJSONCallback = (response) => (_key, value) => {
if (typeof value === "string") return parseModelString(response, value);
if (typeof value === "object" && value !== null) return parseModelTuple(value);
return value;
};
const startReadingFromStream = (response, stream) => {
const reader = stream.getReader();
const progress = (chunk) => {
const { done, value } = chunk;
if (done) return;
processBinaryChunk(response, value);
return reader.read().then(progress);
};
reader.read().then(progress);
};
const createFromReadableStream = (stream) => {
const response = {
_chunks: /* @__PURE__ */ new Map(),
_partialRow: ""
};
response._fromJSON = createFromJSONCallback(response);
startReadingFromStream(response, stream);
const chunk = createPendingChunk();
response._chunks.set(0, chunk);
return chunk;
};
//#endregion
//#region private/client/utils/page.ts
/**
* Downloads an asset from the server, without evaluating it.
*
* @param bundleId - The ID of the bundle.
* @param type - The type of the asset to download.
*
* @returns A promise that resolves once the asset is downloaded.
*/
const loadResource = async (bundleId, type) => {
const extension = {
"main-css": "css",
"main-js": "js",
shared: "js"
}[type];
const assetPrefix = import.meta.env.__BLADE_ASSET_PREFIX;
return new Promise((resolve, reject) => {
const link = document.createElement("link");
link.rel = extension === "css" ? "preload" : "modulepreload";
link.as = extension === "css" ? "style" : "script";
link.onload = resolve;
link.onerror = reject;
link.href = `${assetPrefix}/${getOutputFile(bundleId, extension, type === "shared")}`;
document.head.appendChild(link);
});
};
let SUBSCRIPTIONS = new Array();
/**
* Sends a request for rendering a page to the server, which opens a readable stream.
*
* @param url - The URL of the page to render.
* @param body - The body of the outgoing request.
* @param subscribe - Whether the stream should remain open for future server pushes.
*
* @returns A readable stream of events, with a new event getting submitted for every
* server-side page render.
*/
const createStreamSource = async (url, body, subscribe) => {
const headers = new Headers({
Accept: "text/plain",
[CUSTOM_HEADERS.bundleId]: import.meta.env.__BLADE_BUNDLE_ID
});
if (subscribe) headers.set(CUSTOM_HEADERS.subscribe, "1");
const response = await fetchRetry(url, {
method: "POST",
body,
headers
});
if (!response.ok) throw new Error(await response.text());
if (!response.body) throw new Error("Empty response body");
const reader = response.body.getReader();
if (subscribe) {
SUBSCRIPTIONS.forEach((subscription) => {
subscription.cancel();
subscription.releaseLock();
});
SUBSCRIPTIONS = [];
SUBSCRIPTIONS.push(reader);
}
const listeners = /* @__PURE__ */ new Map();
const decoder = new TextDecoder();
const parser = createParser({ onEvent: (event) => dispatchStreamEvent(event.event, event.data, event.id) });
const dispatchStreamEvent = (type, data, id) => {
return (listeners.get(type) || []).forEach((cb) => cb({
data,
id
}));
};
const startReading = async () => {
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (subscribe && reader !== SUBSCRIPTIONS.at(-1)) {
reader.cancel();
break;
}
const decoded = decoder.decode(value);
parser.feed(decoded);
}
} catch (err) {
if (!(err instanceof TypeError && /load failed/i.test(err.message))) throw err;
} finally {
reader.releaseLock();
}
};
return {
addEventListener: (type, callback) => {
if (!listeners.has(type)) listeners.set(type, []);
listeners.get(type).push(callback);
},
subscribed: subscribe,
startReading
};
};
let BLADE_ROOT = null;
/**
* Receives a React node and renders it at the root of the page.
*
* @param content - The React node to render.
*
* @returns Nothing.
*/
const renderRoot = (content) => {
if (BLADE_ROOT) {
BLADE_ROOT.render(content);
return;
}
BLADE_ROOT = hydrateRoot(document, content, { onRecoverableError(error, errorInfo) {
console.error("Hydration error occurred:", error, errorInfo);
} });
};
/**
* Resolves a new page from the server-side of Blade.
*
* @param path - The path of the page.
* @param subscribe - Whether to subscribe to subsequent updates from the server.
* @param options - Additional options for how to resolve the page.
*
* @returns A promise that resolves to a page if `subscribe` is `false`. If it is `true`,
* the promise will never resolve. Instead, the function will render the subsequent
* updates directly.
*/
const fetchPage = async (path, subscribe, options) => {
const body = new FormData();
if (options && Object.keys(options).length > 0) {
body.append("options", JSON.stringify(omit(options, ["files"])));
if (options.files) for (const [identifier, value] of options.files.entries()) body.append("files", value, identifier);
}
const url = new URL(path, location.origin);
if (url.hash) body.append("hash", url.hash);
const stream = await createStreamSource(url.pathname + url.search, body, subscribe);
return new Promise((resolve) => {
stream.addEventListener("update", async (event) => {
const content = await createFromReadableStream(new Blob([event.data]).stream());
if (stream.subscribed) return renderRoot(content);
resolve(content);
});
stream.addEventListener("update-bundle", (event) => {
mountNewBundle(event.id.split("-").pop(), event.data);
});
stream.startReading();
});
};
const mountNewBundle = async (bundleId, newMarkup) => {
await Promise.all([
loadResource(bundleId, "main-css"),
loadResource(bundleId, "main-js"),
loadResource(bundleId, "shared")
]);
BLADE_ROOT?.unmount();
BLADE_ROOT = null;
const newDocument = new DOMParser().parseFromString(newMarkup, "text/html");
document.documentElement.innerHTML = newDocument.documentElement.innerHTML;
[...Array.from(newDocument.documentElement.attributes)].forEach(({ name, value }) => {
document.documentElement.setAttribute(name, value);
});
for (const oldScript of Array.from(document.querySelectorAll("script.blade-script"))) {
const newScript = document.createElement("script");
for (const attr of Array.from(oldScript.attributes)) newScript.setAttribute(attr.name, attr.value);
document.head.appendChild(newScript);
oldScript.remove();
}
console.debug("Updated client bundles");
};
//#endregion
//#region private/universal/context.ts
/**
* Picks the properties of the available server context that can be serialized and passed
* on to the client-side.
*
* @param serverContext - A server context object.
*
* @returns All server context that can be passed to the client-side.
*/
const getSerializableContext = (serverContext) => ({
url: serverContext.url,
params: serverContext.params,
userAgent: serverContext.userAgent,
geoLocation: serverContext.geoLocation,
addressBarInSync: serverContext.addressBarInSync,
languages: serverContext.languages,
collected: { queries: serverContext.collected.queries.filter((details) => details.type === "write") }
});
//#endregion
//#region private/universal/hooks.ts
const useUniversalContext = () => {
const isNetlify = typeof Netlify !== "undefined";
if (typeof window === "undefined" || isNetlify) {
const serverContext = useContext(RootServerContext);
if (!serverContext) throw new Error("Missing server context in `useUniversalContext`");
return getSerializableContext(serverContext);
}
const clientContext = useContext(RootClientContext);
if (!clientContext) throw new Error("Missing client context in `useUniversalContext`");
const { deferredPromises,...universalContext } = clientContext;
return universalContext;
};
const usePrivateLocation = () => {
const url = new URL(useUniversalContext().url);
const isNetlify = typeof Netlify !== "undefined";
if (typeof window === "undefined" || isNetlify) return url;
return url;
};
//#endregion
//#region public/universal/hooks.ts
const useParams = () => useUniversalContext().params;
const useLocation = () => {
const newLocation = usePrivateLocation();
newLocation.searchParams.delete("page");
newLocation.hash = "";
return newLocation;
};
const useNavigator = () => {
const universalContext = useUniversalContext();
return {
userAgent: universalContext.userAgent,
geoLocation: universalContext.geoLocation,
languages: universalContext.languages
};
};
const usePopulatePathname = () => {
const params = useParams();
return (pathname, extraParams) => {
return populatePathSegments(pathname, Object.assign({}, params, extraParams));
};
};
const useRedirect = () => {
const populatePathname = usePopulatePathname();
const isNetlify = typeof Netlify !== "undefined";
if (typeof window === "undefined" || isNetlify) return (pathname, options) => {
throw { __blade_redirect: populatePathname(pathname, options?.extraParams) };
};
const { transitionPage } = usePageTransition();
return (pathname, options) => {
transitionPage(populatePathname(pathname, options?.extraParams), { immediatelyUpdateQueryParams: options?.immediatelyUpdateQueryParams });
};
};
const useCookie = (name) => {
const isNetlify = typeof Netlify !== "undefined";
if (typeof window === "undefined" || isNetlify) {
const serverContext = useContext(RootServerContext);
if (!serverContext) throw new Error("Missing server context in `useCookie`");
const { cookies } = serverContext;
const value$1 = cookies[name];
const setValue$1 = (value$2, options) => {
return getCookieSetter(serverContext)(name, value$2, options);
};
return [value$1, setValue$1];
}
const value = document.cookie.match(`(^|;)\\s*${name}=([^;]*)`)?.pop() || null;
const setValue = (value$1, options) => {
const shouldDelete = value$1 === null;
const encodedValue = shouldDelete ? "" : encodeURIComponent(value$1);
const components = [`${encodeURIComponent(name)}=${encodedValue}`];
if (shouldDelete) components.push("expires=Thu, 01 Jan 1970 00:00:00 GMT");
else components.push(`max-age=${DEFAULT_COOKIE_MAX_AGE}`);
const path = options?.path || "/";
components.push(`path=${path}`);
document.cookie = components.join("; ");
};
return [value, setValue];
};
//#endregion
//#region private/client/hooks.ts
const usePageTransition = () => {
const cache = useRef(/* @__PURE__ */ new Map());
const clientContext = useContext(RootClientContext);
if (!clientContext) throw new Error("Missing client context in `usePageTransition`");
const privateLocationRef = usePrivateLocationRef();
const getCacheEntry = (path) => {
const cacheEntry = cache.current.get(path);
const maxAge = Date.now() - 1e4;
return cacheEntry && cacheEntry.time > maxAge ? cacheEntry : null;
};
const primePageCache = (path) => {
if (getCacheEntry(path)) return;
const promise = fetchPage(path, false);
cache.current.set(path, {
body: promise,
time: Date.now()
});
};
const transitionPage = (path, options) => {
const privateLocation = privateLocationRef.current;
if (options?.acceptCache && !IS_CLIENT_DEV) getCacheEntry(path)?.body.then((page) => renderRoot(page));
if (options?.immediatelyUpdateQueryParams) {
const url = new URL(path, privateLocation.origin);
history.replaceState(history.state, "", url);
clientContext.setClientQueryParams(url.search);
}
if (!window.navigator.onLine) throw new Error("Device is not online to perform manual page transition");
fetchPage(path, true, omit(options || {}, ["acceptCache", "immediatelyUpdateQueryParams"]));
};
return {
primePageCache,
transitionPage
};
};
/**
* A React reference acting as a replacement for `window.location`. Due to being a
* reference, the value is always up-to-date, even inside a callback.
*/
const usePrivateLocationRef = () => {
const privateLocation = usePrivateLocation();
privateLocation.pathname = usePopulatePathname()(privateLocation.pathname);
const ref = useRef(privateLocation);
useEffect(() => {
ref.current = privateLocation;
}, [privateLocation, privateLocation.href]);
return ref;
};
/**
* Behaves exactly like `useMemo`, except that the provided factory receives the
* previously memorized value as an argument, which allows for accumulating state, just
* like the `.reduce` method on an array would.
*
* Furthermore, in addition to returning the computed value (like `useMemo`), the hook
* also returns a function for updating it from the outside.
*
* @param factory - A function for computing a value.
* @param deps - A list of dependencies. If one of them changes, the factory function
* will be called and the memorized value will be re-computed.
*
* @returns The computed value, and a function for setting it.
*/
const useReduce = (rootFactory, deps) => {
const value = useRef(rootFactory(void 0));
const [renderingCount, setRenderingCount] = useState(0);
const setValue = (childFactory) => {
value.current = childFactory(value.current);
setRenderingCount(renderingCount + 1);
};
useMemo(() => {
value.current = rootFactory(value.current);
}, deps);
return [value.current, setValue];
};
//#endregion
export { RootClientContext as _, useLocation as a, usePopulatePathname as c, useUniversalContext as d, getSerializableContext as f, construct as g, assign as h, useCookie as i, useRedirect as l, FORM_TARGET_PREFIX as m, usePrivateLocationRef as n, useNavigator as o, fetchPage as p, useReduce as r, useParams as s, usePageTransition as t, usePrivateLocation as u };