${statusCode ? `${statusCode}: ` : ""}${title}
${body || `
Path: ${escape(pathname)}
`}
`;
}
const DEFAULT_404_ROUTE = {
component: DEFAULT_404_COMPONENT,
params: [],
pattern: /^\/404\/?$/,
prerender: false,
pathname: "/404",
segments: [[{ content: "404", dynamic: false, spread: false }]],
type: "page",
route: "/404",
fallbackRoutes: [],
isIndex: false,
origin: "internal",
distURL: []
};
async function default404Page({ pathname }) {
return new Response(
template({
statusCode: 404,
title: "Not found",
tabTitle: "404: Not Found",
pathname
}),
{ status: 404, headers: { "Content-Type": "text/html" } }
);
}
default404Page.isAstroComponentFactory = true;
const default404Instance = {
default: default404Page
};
const ROUTE404_RE = /^\/404\/?$/;
const ROUTE500_RE = /^\/500\/?$/;
function isRoute404(route) {
return ROUTE404_RE.test(route);
}
function isRoute500(route) {
return ROUTE500_RE.test(route);
}
function findRouteToRewrite({
payload,
routes,
request,
trailingSlash,
buildFormat,
base,
outDir
}) {
let newUrl = void 0;
if (payload instanceof URL) {
newUrl = payload;
} else if (payload instanceof Request) {
newUrl = new URL(payload.url);
} else {
newUrl = new URL(collapseDuplicateSlashes(payload), new URL(request.url).origin);
}
const { pathname, resolvedUrlPathname } = normalizeRewritePathname(
newUrl.pathname,
base,
trailingSlash,
buildFormat
);
newUrl.pathname = resolvedUrlPathname;
const decodedPathname = decodeURI(pathname);
if (isRoute404(decodedPathname)) {
const errorRoute = routes.find((route) => route.route === "/404");
if (errorRoute) {
return { routeData: errorRoute, newUrl, pathname: decodedPathname };
}
}
if (isRoute500(decodedPathname)) {
const errorRoute = routes.find((route) => route.route === "/500");
if (errorRoute) {
return { routeData: errorRoute, newUrl, pathname: decodedPathname };
}
}
let foundRoute;
for (const route of routes) {
if (route.pattern.test(decodedPathname)) {
if (route.params && route.params.length !== 0 && route.distURL && route.distURL.length !== 0) {
if (!route.distURL.find(
(url) => url.href.replace(outDir.toString(), "").replace(/(?:\/index\.html|\.html)$/, "") === trimSlashes(pathname)
)) {
continue;
}
}
foundRoute = route;
break;
}
}
if (foundRoute) {
return {
routeData: foundRoute,
newUrl,
pathname: decodedPathname
};
} else {
const custom404 = routes.find((route) => route.route === "/404");
if (custom404) {
return { routeData: custom404, newUrl, pathname };
} else {
return { routeData: DEFAULT_404_ROUTE, newUrl, pathname };
}
}
}
function copyRequest(newUrl, oldRequest, isPrerendered, logger, routePattern) {
if (oldRequest.bodyUsed) {
throw new AstroError(RewriteWithBodyUsed);
}
return createRequest({
url: newUrl,
method: oldRequest.method,
body: oldRequest.body,
isPrerendered,
logger,
headers: isPrerendered ? {} : oldRequest.headers,
routePattern,
init: {
referrer: oldRequest.referrer,
referrerPolicy: oldRequest.referrerPolicy,
mode: oldRequest.mode,
credentials: oldRequest.credentials,
cache: oldRequest.cache,
redirect: oldRequest.redirect,
integrity: oldRequest.integrity,
signal: oldRequest.signal,
keepalive: oldRequest.keepalive,
// https://fetch.spec.whatwg.org/#dom-request-duplex
// @ts-expect-error It isn't part of the types, but undici accepts it and it allows to carry over the body to a new request
duplex: "half"
}
});
}
function setOriginPathname(request, pathname, trailingSlash, buildFormat) {
if (!pathname) {
pathname = "/";
}
const shouldAppendSlash = shouldAppendForwardSlash(trailingSlash, buildFormat);
let finalPathname;
if (pathname === "/") {
finalPathname = "/";
} else if (shouldAppendSlash) {
finalPathname = appendForwardSlash(pathname);
} else {
finalPathname = removeTrailingForwardSlash(pathname);
}
Reflect.set(request, originPathnameSymbol, encodeURIComponent(finalPathname));
}
function getOriginPathname(request) {
const origin = Reflect.get(request, originPathnameSymbol);
if (origin) {
return decodeURIComponent(origin);
}
return new URL(request.url).pathname;
}
function normalizeRewritePathname(urlPathname, base, trailingSlash, buildFormat) {
let pathname = collapseDuplicateSlashes(urlPathname);
const shouldAppendSlash = shouldAppendForwardSlash(trailingSlash, buildFormat);
if (base !== "/") {
const isBasePathRequest = urlPathname === base || urlPathname === removeTrailingForwardSlash(base);
if (isBasePathRequest) {
pathname = shouldAppendSlash ? "/" : "";
} else if (urlPathname.startsWith(base)) {
pathname = shouldAppendSlash ? appendForwardSlash(urlPathname) : removeTrailingForwardSlash(urlPathname);
pathname = pathname.slice(base.length);
}
}
if (!pathname.startsWith("/") && shouldAppendSlash && urlPathname.endsWith("/")) {
pathname = prependForwardSlash(pathname);
}
if (pathname === "/" && base !== "/" && !shouldAppendSlash) {
pathname = "";
}
if (buildFormat === "file") {
pathname = pathname.replace(/\.html$/, "");
}
let resolvedUrlPathname;
if (base !== "/" && (pathname === "" || pathname === "/") && !shouldAppendSlash) {
resolvedUrlPathname = removeTrailingForwardSlash(base);
} else {
resolvedUrlPathname = joinPaths(...[base, pathname].filter(Boolean));
}
return { pathname, resolvedUrlPathname };
}
const NOOP_ACTIONS_MOD = {
server: {}
};
function defineMiddleware(fn) {
return fn;
}
const FORM_CONTENT_TYPES = [
"application/x-www-form-urlencoded",
"multipart/form-data",
"text/plain"
];
const SAFE_METHODS = ["GET", "HEAD", "OPTIONS"];
function createOriginCheckMiddleware() {
return defineMiddleware((context, next) => {
const { request, url, isPrerendered } = context;
if (isPrerendered) {
return next();
}
if (SAFE_METHODS.includes(request.method)) {
return next();
}
const isSameOrigin = request.headers.get("origin") === url.origin;
const hasContentType = request.headers.has("content-type");
if (hasContentType) {
const formLikeHeader = hasFormLikeHeader(request.headers.get("content-type"));
if (formLikeHeader && !isSameOrigin) {
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
status: 403
});
}
} else {
if (!isSameOrigin) {
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
status: 403
});
}
}
return next();
});
}
function hasFormLikeHeader(contentType) {
if (contentType) {
for (const FORM_CONTENT_TYPE of FORM_CONTENT_TYPES) {
if (contentType.toLowerCase().includes(FORM_CONTENT_TYPE)) {
return true;
}
}
}
return false;
}
const NOOP_MIDDLEWARE_FN = async (_ctx, next) => {
const response = await next();
response.headers.set(NOOP_MIDDLEWARE_HEADER, "true");
return response;
};
const RedirectComponentInstance = {
default() {
return new Response(null, {
status: 301
});
}
};
const RedirectSinglePageBuiltModule = {
page: () => Promise.resolve(RedirectComponentInstance),
onRequest: (_, next) => next()
};
function sanitizeParams(params) {
return Object.fromEntries(
Object.entries(params).map(([key, value]) => {
if (typeof value === "string") {
return [key, value.normalize().replace(/#/g, "%23").replace(/\?/g, "%3F")];
}
return [key, value];
})
);
}
function getParameter(part, params) {
if (part.spread) {
return params[part.content.slice(3)] || "";
}
if (part.dynamic) {
if (!params[part.content]) {
throw new TypeError(`Missing parameter: ${part.content}`);
}
return params[part.content];
}
return part.content.normalize().replace(/\?/g, "%3F").replace(/#/g, "%23").replace(/%5B/g, "[").replace(/%5D/g, "]");
}
function getSegment(segment, params) {
const segmentPath = segment.map((part) => getParameter(part, params)).join("");
return segmentPath ? collapseDuplicateLeadingSlashes("/" + segmentPath) : "";
}
function getRouteGenerator(segments, addTrailingSlash) {
return (params) => {
const sanitizedParams = sanitizeParams(params);
let trailing = "";
if (addTrailingSlash === "always" && segments.length) {
trailing = "/";
}
const path = segments.map((segment) => getSegment(segment, sanitizedParams)).join("") + trailing;
return path || "/";
};
}
const VALID_PARAM_TYPES = ["string", "undefined"];
function validateGetStaticPathsParameter([key, value], route) {
if (!VALID_PARAM_TYPES.includes(typeof value)) {
throw new AstroError({
...GetStaticPathsInvalidRouteParam,
message: GetStaticPathsInvalidRouteParam.message(key, value, typeof value),
location: {
file: route
}
});
}
}
function stringifyParams(params, route, trailingSlash) {
const validatedParams = {};
for (const [key, value] of Object.entries(params)) {
validateGetStaticPathsParameter([key, value], route.component);
if (value !== void 0) {
validatedParams[key] = trimSlashes(value);
}
}
return getRouteGenerator(route.segments, trailingSlash)(validatedParams);
}
function validateDynamicRouteModule(mod, {
ssr,
route
}) {
if ((!ssr || route.prerender) && !mod.getStaticPaths) {
throw new AstroError({
...GetStaticPathsRequired,
location: { file: route.component }
});
}
}
function validateGetStaticPathsResult(result, route) {
if (!Array.isArray(result)) {
throw new AstroError({
...InvalidGetStaticPathsReturn,
message: InvalidGetStaticPathsReturn.message(typeof result),
location: {
file: route.component
}
});
}
result.forEach((pathObject) => {
if (typeof pathObject === "object" && Array.isArray(pathObject) || pathObject === null) {
throw new AstroError({
...InvalidGetStaticPathsEntry,
message: InvalidGetStaticPathsEntry.message(
Array.isArray(pathObject) ? "array" : typeof pathObject
)
});
}
if (pathObject.params === void 0 || pathObject.params === null || pathObject.params && Object.keys(pathObject.params).length === 0) {
throw new AstroError({
...GetStaticPathsExpectedParams,
location: {
file: route.component
}
});
}
});
}
function generatePaginateFunction(routeMatch, base, trailingSlash) {
return function paginateUtility(data, args = {}) {
const generate = getRouteGenerator(routeMatch.segments, trailingSlash);
let { pageSize: _pageSize, params: _params, props: _props } = args;
const pageSize = _pageSize || 10;
const paramName = "page";
const additionalParams = _params || {};
const additionalProps = _props || {};
let includesFirstPageNumber;
if (routeMatch.params.includes(`...${paramName}`)) {
includesFirstPageNumber = false;
} else if (routeMatch.params.includes(`${paramName}`)) {
includesFirstPageNumber = true;
} else {
throw new AstroError({
...PageNumberParamNotFound,
message: PageNumberParamNotFound.message(paramName)
});
}
const lastPage = Math.max(1, Math.ceil(data.length / pageSize));
const result = [...Array(lastPage).keys()].map((num) => {
const pageNum = num + 1;
const start = pageSize === Number.POSITIVE_INFINITY ? 0 : (pageNum - 1) * pageSize;
const end = Math.min(start + pageSize, data.length);
const params = {
...additionalParams,
[paramName]: includesFirstPageNumber || pageNum > 1 ? String(pageNum) : void 0
};
const current = addRouteBase(generate({ ...params }), base);
const next = pageNum === lastPage ? void 0 : addRouteBase(generate({ ...params, page: String(pageNum + 1) }), base);
const prev = pageNum === 1 ? void 0 : addRouteBase(
generate({
...params,
page: !includesFirstPageNumber && pageNum - 1 === 1 ? void 0 : String(pageNum - 1)
}),
base
);
const first = pageNum === 1 ? void 0 : addRouteBase(
generate({
...params,
page: includesFirstPageNumber ? "1" : void 0
}),
base
);
const last = pageNum === lastPage ? void 0 : addRouteBase(generate({ ...params, page: String(lastPage) }), base);
return {
params,
props: {
...additionalProps,
page: {
data: data.slice(start, end),
start,
end: end - 1,
size: pageSize,
total: data.length,
currentPage: pageNum,
lastPage,
url: { current, next, prev, first, last }
}
}
};
});
return result;
};
}
function addRouteBase(route, base) {
let routeWithBase = joinPaths(base, route);
if (routeWithBase === "") routeWithBase = "/";
return routeWithBase;
}
async function callGetStaticPaths({
mod,
route,
routeCache,
ssr,
base,
trailingSlash
}) {
const cached = routeCache.get(route);
if (!mod) {
throw new Error("This is an error caused by Astro and not your code. Please file an issue.");
}
if (cached?.staticPaths) {
return cached.staticPaths;
}
validateDynamicRouteModule(mod, { ssr, route });
if (ssr && !route.prerender) {
const entry = Object.assign([], { keyed: /* @__PURE__ */ new Map() });
routeCache.set(route, { ...cached, staticPaths: entry });
return entry;
}
let staticPaths = [];
if (!mod.getStaticPaths) {
throw new Error("Unexpected Error.");
}
staticPaths = await mod.getStaticPaths({
// Q: Why the cast?
// A: So users downstream can have nicer typings, we have to make some sacrifice in our internal typings, which necessitate a cast here
paginate: generatePaginateFunction(route, base, trailingSlash),
routePattern: route.route
});
validateGetStaticPathsResult(staticPaths, route);
const keyedStaticPaths = staticPaths;
keyedStaticPaths.keyed = /* @__PURE__ */ new Map();
for (const sp of keyedStaticPaths) {
const paramsKey = stringifyParams(sp.params, route, trailingSlash);
keyedStaticPaths.keyed.set(paramsKey, sp);
}
routeCache.set(route, { ...cached, staticPaths: keyedStaticPaths });
return keyedStaticPaths;
}
class RouteCache {
logger;
cache = {};
runtimeMode;
constructor(logger, runtimeMode = "production") {
this.logger = logger;
this.runtimeMode = runtimeMode;
}
/** Clear the cache. */
clearAll() {
this.cache = {};
}
set(route, entry) {
const key = this.key(route);
if (this.runtimeMode === "production" && this.cache[key]?.staticPaths) {
this.logger.warn(null, `Internal Warning: route cache overwritten. (${key})`);
}
this.cache[key] = entry;
}
get(route) {
return this.cache[this.key(route)];
}
key(route) {
return `${route.route}_${route.component}`;
}
}
function findPathItemByKey(staticPaths, params, route, logger, trailingSlash) {
const paramsKey = stringifyParams(params, route, trailingSlash);
const matchedStaticPath = staticPaths.keyed.get(paramsKey);
if (matchedStaticPath) {
return matchedStaticPath;
}
logger.debug("router", `findPathItemByKey() - Unexpected cache miss looking for ${paramsKey}`);
}
function getPattern(segments, base, addTrailingSlash) {
const pathname = segments.map((segment) => {
if (segment.length === 1 && segment[0].spread) {
return "(?:\\/(.*?))?";
} else {
return "\\/" + segment.map((part) => {
if (part.spread) {
return "(.*?)";
} else if (part.dynamic) {
return "([^/]+?)";
} else {
return part.content.normalize().replace(/\?/g, "%3F").replace(/#/g, "%23").replace(/%5B/g, "[").replace(/%5D/g, "]").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
}).join("");
}
}).join("");
const trailing = addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : "$";
let initial = "\\/";
if (addTrailingSlash === "never" && base !== "/") {
initial = "";
}
return new RegExp(`^${pathname || initial}${trailing}`);
}
function getTrailingSlashPattern(addTrailingSlash) {
if (addTrailingSlash === "always") {
return "\\/$";
}
if (addTrailingSlash === "never") {
return "$";
}
return "\\/?$";
}
const SERVER_ISLAND_ROUTE = "/_server-islands/[name]";
const SERVER_ISLAND_COMPONENT = "_server-islands.astro";
function badRequest(reason) {
return new Response(null, {
status: 400,
statusText: "Bad request: " + reason
});
}
const DEFAULT_BODY_SIZE_LIMIT = 1024 * 1024;
async function getRequestData(request, bodySizeLimit = DEFAULT_BODY_SIZE_LIMIT) {
switch (request.method) {
case "GET": {
const url = new URL(request.url);
const params = url.searchParams;
if (!params.has("s") || !params.has("e") || !params.has("p")) {
return badRequest("Missing required query parameters.");
}
const encryptedSlots = params.get("s");
return {
encryptedComponentExport: params.get("e"),
encryptedProps: params.get("p"),
encryptedSlots
};
}
case "POST": {
try {
const body = await readBodyWithLimit(request, bodySizeLimit);
const raw = new TextDecoder().decode(body);
const data = JSON.parse(raw);
if (Object.hasOwn(data, "slots") && typeof data.slots === "object") {
return badRequest("Plaintext slots are not allowed. Slots must be encrypted.");
}
if (Object.hasOwn(data, "componentExport") && typeof data.componentExport === "string") {
return badRequest(
"Plaintext componentExport is not allowed. componentExport must be encrypted."
);
}
return data;
} catch (e) {
if (e instanceof BodySizeLimitError) {
return new Response(null, {
status: 413,
statusText: e.message
});
}
if (e instanceof SyntaxError) {
return badRequest("Request format is invalid.");
}
throw e;
}
}
default: {
return new Response(null, { status: 405 });
}
}
}
function createEndpoint(manifest) {
const page = async (result) => {
const params = result.params;
if (!params.name) {
return new Response(null, {
status: 400,
statusText: "Bad request"
});
}
const componentId = params.name;
const data = await getRequestData(result.request, manifest.serverIslandBodySizeLimit);
if (data instanceof Response) {
return data;
}
const serverIslandMappings = await manifest.serverIslandMappings?.();
const serverIslandMap = await serverIslandMappings?.serverIslandMap;
let imp = serverIslandMap?.get(componentId);
if (!imp) {
return new Response(null, {
status: 404,
statusText: "Not found"
});
}
const key = await manifest.key;
let componentExport;
try {
componentExport = await decryptString(
key,
data.encryptedComponentExport,
`export:${componentId}`
);
} catch (_e) {
return badRequest("Encrypted componentExport value is invalid.");
}
const encryptedProps = data.encryptedProps;
let props = {};
if (encryptedProps !== "") {
try {
const propString = await decryptString(key, encryptedProps, `props:${componentId}`);
props = JSON.parse(propString);
} catch (_e) {
return badRequest("Encrypted props value is invalid.");
}
}
let decryptedSlots = {};
const encryptedSlots = data.encryptedSlots;
if (encryptedSlots !== "") {
try {
const slotsString = await decryptString(key, encryptedSlots, `slots:${componentId}`);
decryptedSlots = JSON.parse(slotsString);
} catch (_e) {
return badRequest("Encrypted slots value is invalid.");
}
}
const componentModule = await imp();
let Component = componentModule[componentExport];
const slots = {};
for (const prop in decryptedSlots) {
slots[prop] = createSlotValueFromString(decryptedSlots[prop]);
}
result.response.headers.set("X-Robots-Tag", "noindex");
if (isAstroComponentFactory(Component)) {
const ServerIsland = Component;
Component = function(...args) {
return ServerIsland.apply(this, args);
};
Object.assign(Component, ServerIsland);
Component.propagation = "self";
}
return renderTemplate`${renderComponent(result, "Component", Component, props, slots)}`;
};
page.isAstroComponentFactory = true;
const instance = {
default: page,
partial: true
};
return instance;
}
function createDefaultRoutes(manifest) {
const root = new URL(manifest.rootDir);
return [
{
instance: default404Instance,
matchesComponent: (filePath) => filePath.href === new URL(DEFAULT_404_COMPONENT, root).href,
route: DEFAULT_404_ROUTE.route,
component: DEFAULT_404_COMPONENT
},
{
instance: createEndpoint(manifest),
matchesComponent: (filePath) => filePath.href === new URL(SERVER_ISLAND_COMPONENT, root).href,
route: SERVER_ISLAND_ROUTE,
component: SERVER_ISLAND_COMPONENT
}
];
}
function deserializeManifest(serializedManifest, routesList) {
const routes = [];
if (serializedManifest.routes) {
for (const serializedRoute of serializedManifest.routes) {
routes.push({
...serializedRoute,
routeData: deserializeRouteData(serializedRoute.routeData)
});
const route = serializedRoute;
route.routeData = deserializeRouteData(serializedRoute.routeData);
}
}
const assets = new Set(serializedManifest.assets);
const componentMetadata = new Map(serializedManifest.componentMetadata);
const inlinedScripts = new Map(serializedManifest.inlinedScripts);
const clientDirectives = new Map(serializedManifest.clientDirectives);
const key = decodeKey(serializedManifest.key);
return {
// in case user middleware exists, this no-op middleware will be reassigned (see plugin-ssr.ts)
middleware() {
return { onRequest: NOOP_MIDDLEWARE_FN };
},
...serializedManifest,
rootDir: new URL(serializedManifest.rootDir),
srcDir: new URL(serializedManifest.srcDir),
publicDir: new URL(serializedManifest.publicDir),
outDir: new URL(serializedManifest.outDir),
cacheDir: new URL(serializedManifest.cacheDir),
buildClientDir: new URL(serializedManifest.buildClientDir),
buildServerDir: new URL(serializedManifest.buildServerDir),
assets,
componentMetadata,
inlinedScripts,
clientDirectives,
routes,
key
};
}
function deserializeRouteData(rawRouteData) {
return {
route: rawRouteData.route,
type: rawRouteData.type,
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp
// This pattern is serialized from Astro's own route manifest.
pattern: new RegExp(rawRouteData.pattern),
params: rawRouteData.params,
component: rawRouteData.component,
pathname: rawRouteData.pathname || void 0,
segments: rawRouteData.segments,
prerender: rawRouteData.prerender,
redirect: rawRouteData.redirect,
redirectRoute: rawRouteData.redirectRoute ? deserializeRouteData(rawRouteData.redirectRoute) : void 0,
fallbackRoutes: rawRouteData.fallbackRoutes.map((fallback) => {
return deserializeRouteData(fallback);
}),
isIndex: rawRouteData.isIndex,
origin: rawRouteData.origin,
distURL: rawRouteData.distURL
};
}
function deserializeRouteInfo(rawRouteInfo) {
return {
styles: rawRouteInfo.styles,
file: rawRouteInfo.file,
links: rawRouteInfo.links,
scripts: rawRouteInfo.scripts,
routeData: deserializeRouteData(rawRouteInfo.routeData)
};
}
class NodePool {
textPool = [];
htmlStringPool = [];
componentPool = [];
instructionPool = [];
maxSize;
enableStats;
stats = {
acquireFromPool: 0,
acquireNew: 0,
released: 0,
releasedDropped: 0
};
/**
* Creates a new object pool for queue nodes.
*
* @param maxSize - Maximum number of nodes to keep in the pool (default: 1000).
* The cap is shared across all typed sub-pools.
* @param enableStats - Enable statistics tracking (default: false for performance)
*/
constructor(maxSize = 1e3, enableStats = false) {
this.maxSize = maxSize;
this.enableStats = enableStats;
}
/**
* Acquires a queue node from the pool or creates a new one if the pool is empty.
* Pops from the type-specific sub-pool to reuse an existing object when available.
*
* @param type - The type of queue node to acquire
* @param content - Optional content to set on the node (for text or html-string types)
* @returns A queue node ready to be populated with data
*/
acquire(type, content) {
const pooledNode = this.popFromTypedPool(type);
if (pooledNode) {
if (this.enableStats) {
this.stats.acquireFromPool = this.stats.acquireFromPool + 1;
}
this.resetNodeContent(pooledNode, type, content);
return pooledNode;
}
if (this.enableStats) {
this.stats.acquireNew = this.stats.acquireNew + 1;
}
return this.createNode(type, content);
}
/**
* Creates a new node of the specified type with the given content.
* Helper method to reduce branching in acquire().
*/
createNode(type, content = "") {
switch (type) {
case "text":
return { type: "text", content };
case "html-string":
return { type: "html-string", html: content };
case "component":
return { type: "component", instance: void 0 };
case "instruction":
return { type: "instruction", instruction: void 0 };
}
}
/**
* Pops a node from the type-specific sub-pool.
* Returns undefined if the sub-pool for the requested type is empty.
*/
popFromTypedPool(type) {
switch (type) {
case "text":
return this.textPool.pop();
case "html-string":
return this.htmlStringPool.pop();
case "component":
return this.componentPool.pop();
case "instruction":
return this.instructionPool.pop();
}
}
/**
* Resets the content/value field on a reused pooled node.
* The type discriminant is already correct since we pop from the matching sub-pool.
*/
resetNodeContent(node, type, content) {
switch (type) {
case "text":
node.content = content ?? "";
break;
case "html-string":
node.html = content ?? "";
break;
case "component":
node.instance = void 0;
break;
case "instruction":
node.instruction = void 0;
break;
}
}
/**
* Returns the total number of nodes across all typed sub-pools.
*/
totalPoolSize() {
return this.textPool.length + this.htmlStringPool.length + this.componentPool.length + this.instructionPool.length;
}
/**
* Releases a queue node back to the pool for reuse.
* If the pool is at max capacity, the node is discarded (will be GC'd).
*
* @param node - The node to release back to the pool
*/
release(node) {
if (this.totalPoolSize() >= this.maxSize) {
if (this.enableStats) {
this.stats.releasedDropped = this.stats.releasedDropped + 1;
}
return;
}
switch (node.type) {
case "text":
node.content = "";
this.textPool.push(node);
break;
case "html-string":
node.html = "";
this.htmlStringPool.push(node);
break;
case "component":
node.instance = void 0;
this.componentPool.push(node);
break;
case "instruction":
node.instruction = void 0;
this.instructionPool.push(node);
break;
}
if (this.enableStats) {
this.stats.released = this.stats.released + 1;
}
}
/**
* Releases all nodes in an array back to the pool.
* This is a convenience method for releasing multiple nodes at once.
*
* @param nodes - Array of nodes to release
*/
releaseAll(nodes) {
for (const node of nodes) {
this.release(node);
}
}
/**
* Clears all typed sub-pools, discarding all cached nodes.
* This can be useful if you want to free memory after a large render.
*/
clear() {
this.textPool.length = 0;
this.htmlStringPool.length = 0;
this.componentPool.length = 0;
this.instructionPool.length = 0;
}
/**
* Gets the current total number of nodes across all typed sub-pools.
* Useful for monitoring pool usage and tuning maxSize.
*
* @returns Number of nodes currently available in the pool
*/
size() {
return this.totalPoolSize();
}
/**
* Gets pool statistics for debugging.
*
* @returns Pool usage statistics including computed metrics
*/
getStats() {
return {
...this.stats,
poolSize: this.totalPoolSize(),
maxSize: this.maxSize,
hitRate: this.stats.acquireFromPool + this.stats.acquireNew > 0 ? this.stats.acquireFromPool / (this.stats.acquireFromPool + this.stats.acquireNew) * 100 : 0
};
}
/**
* Resets pool statistics.
*/
resetStats() {
this.stats = {
acquireFromPool: 0,
acquireNew: 0,
released: 0,
releasedDropped: 0
};
}
}
class HTMLStringCache {
cache = /* @__PURE__ */ new Map();
maxSize;
constructor(maxSize = 1e3) {
this.maxSize = maxSize;
this.warm(COMMON_HTML_PATTERNS);
}
/**
* Get or create an HTMLString for the given content.
* If cached, the existing object is returned and moved to end (most recently used).
* If not cached, a new HTMLString is created, cached, and returned.
*
* @param content - The HTML string content
* @returns HTMLString object (cached or newly created)
*/
getOrCreate(content) {
const cached = this.cache.get(content);
if (cached) {
this.cache.delete(content);
this.cache.set(content, cached);
return cached;
}
const htmlString = new HTMLString(content);
this.cache.set(content, htmlString);
if (this.cache.size > this.maxSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey !== void 0) {
this.cache.delete(firstKey);
}
}
return htmlString;
}
/**
* Get current cache size
*/
size() {
return this.cache.size;
}
/**
* Pre-warms the cache with common HTML patterns.
* This ensures first-render cache hits for frequently used tags.
*
* @param patterns - Array of HTML strings to pre-cache
*/
warm(patterns) {
for (const pattern of patterns) {
if (!this.cache.has(pattern)) {
this.cache.set(pattern, new HTMLString(pattern));
}
}
}
/**
* Clear the entire cache
*/
clear() {
this.cache.clear();
}
}
const COMMON_HTML_PATTERNS = [
// Structural elements
"