diff --git a/scripts/lint/test-typecheck-baseline.json b/scripts/lint/test-typecheck-baseline.json index 377f4691f7..65e43b2529 100644 --- a/scripts/lint/test-typecheck-baseline.json +++ b/scripts/lint/test-typecheck-baseline.json @@ -32,7 +32,6 @@ "src/mcp/server.test.ts", "src/middleware/builtin/security/security-headers.test.ts", "src/middleware/core/pipeline/composer.test.ts", - "src/modules/import-map/preloader.test.ts", "src/modules/react-loader/ssr-module-loader.stress.test.ts", "src/platform/adapters/fs/veryfront/directory-operations.test.ts", "src/platform/adapters/redis/node.test.ts", diff --git a/src/build/production-build/templates.ts b/src/build/production-build/templates.ts index 60d4495252..5222ed6a22 100644 --- a/src/build/production-build/templates.ts +++ b/src/build/production-build/templates.ts @@ -14,4 +14,4 @@ export const CLIENT_ROUTER_BUNDLE: string | undefined = 'var __defProp = Object.defineProperty;\nvar __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;\nvar __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);\n\n// src/rendering/client/browser-stubs/logger.ts\nfunction noop() {\n}\nvar logger = {\n debug: noop,\n info: console.log.bind(console),\n warn: console.warn.bind(console),\n error: console.error.bind(console),\n component: () => logger\n};\nvar rendererLogger = logger;\nvar PREFETCH_MAX_SIZE_BYTES = 200 * 1024;\n\n// src/rendering/client/navigation-store.ts\nvar STORE_KEY = /* @__PURE__ */ Symbol.for("veryfront.navigation.store.v1");\nfunction getNavigationStore() {\n const holder = globalThis;\n const existing = holder[STORE_KEY];\n if (existing) return existing;\n const listeners = /* @__PURE__ */ new Set();\n let navigator = null;\n const store = {\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n getHref() {\n const loc = globalThis.location;\n return loc ? `${loc.pathname}${loc.search}${loc.hash}` : "/";\n },\n notify() {\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch {\n }\n }\n },\n navigate(href, options) {\n if (navigator) return navigator(href, options);\n globalThis.location?.assign(href);\n return Promise.resolve();\n },\n setNavigator(next) {\n navigator = next;\n }\n };\n holder[STORE_KEY] = store;\n return store;\n}\n\n// src/rendering/client/router.ts\nimport ReactDOM from "react-dom/client";\n\n// src/html/managed-head-protocol.ts\nvar HEAD_PROVENANCE_ATTRIBUTE = "data-vf-head";\nvar HEAD_LEGACY_MANAGED_ATTRIBUTE = "data-veryfront-managed";\nvar HEAD_CONTENT_HASH_ATTRIBUTE = "data-vf-hash";\nvar HEAD_REACT_MANAGED_ATTRIBUTE = "data-vf-react-head";\nvar HEAD_REACT_OWNER_ATTRIBUTE = "data-vf-react-head-owner";\nvar HEAD_ROUTE_MANAGED_ATTRIBUTE = "data-vf-route-head";\nvar HEAD_SERVER_COMMIT_ATTRIBUTE = "data-vf-server-head-commit";\nvar HEAD_SHELL_PROVENANCE_ATTRIBUTE = "data-vf-shell-head";\nvar HEAD_SSR_PAYLOAD_ATTRIBUTE = "data-vf-ssr-head";\nvar MAX_MANAGED_HEAD_ENTRIES = 128;\nvar MAX_MANAGED_HEAD_BYTES = 2 * 1024 * 1024;\nvar MAX_MANAGED_HEAD_PAYLOAD_BYTES = MAX_MANAGED_HEAD_BYTES * 2;\nvar REACT_HEAD_ATTRIBUTE_NAMES = {\n charSet: "charset",\n className: "class",\n crossOrigin: "crossorigin",\n fetchPriority: "fetchpriority",\n htmlFor: "for",\n httpEquiv: "http-equiv",\n imageSizes: "imagesizes",\n imageSrcSet: "imagesrcset",\n noModule: "nomodule",\n referrerPolicy: "referrerpolicy"\n};\nvar SINGLETON_META_KEYS = /* @__PURE__ */ new Set([\n "description",\n "robots",\n "viewport",\n "referrer",\n "color-scheme",\n "application-name",\n "generator",\n "og:title",\n "og:description",\n "og:url",\n "og:type",\n "og:site_name",\n "og:locale",\n "twitter:card",\n "twitter:site",\n "twitter:creator",\n "twitter:title",\n "twitter:description",\n "twitter:image",\n "twitter:image:alt"\n]);\nvar SINGLETON_LINK_RELS = /* @__PURE__ */ new Set([\n "canonical",\n "manifest",\n "amphtml"\n]);\nvar SUPPORTED_MANAGED_HEAD_TAGS = /* @__PURE__ */ new Set([\n "title",\n "meta",\n "link",\n "style",\n "script"\n]);\nvar HEAD_ATTRIBUTE_NAME_PATTERN = /^[A-Za-z_:][A-Za-z0-9_.:-]*$/;\nvar MAX_HEAD_PROP_ENTRIES = 128;\nvar MAX_HEAD_ATTRIBUTE_NAME_BYTES = 256;\nvar MAX_HEAD_ATTRIBUTE_VALUE_BYTES = 64 * 1024;\nvar MAX_HEAD_ATTRIBUTE_BYTES = 1024 * 1024;\nvar MAX_HEAD_CONTENT_BYTES = 1024 * 1024;\nvar headTextEncoder = new TextEncoder();\nvar BOOLEAN_HEAD_ATTRIBUTES = /* @__PURE__ */ new Set([\n "async",\n "defer",\n "disabled",\n "itemscope",\n "nomodule"\n]);\nfunction isHeadFrameworkAttribute(name) {\n switch (name.toLowerCase()) {\n case HEAD_PROVENANCE_ATTRIBUTE:\n case HEAD_LEGACY_MANAGED_ATTRIBUTE:\n case HEAD_CONTENT_HASH_ATTRIBUTE:\n case HEAD_REACT_MANAGED_ATTRIBUTE:\n case HEAD_REACT_OWNER_ATTRIBUTE:\n case HEAD_ROUTE_MANAGED_ATTRIBUTE:\n case HEAD_SERVER_COMMIT_ATTRIBUTE:\n case HEAD_SHELL_PROVENANCE_ATTRIBUTE:\n case HEAD_SSR_PAYLOAD_ATTRIBUTE:\n return true;\n default:\n return false;\n }\n}\nfunction normalizeHeadIdentityValue(value) {\n const normalized = value?.trim().toLowerCase();\n return normalized || void 0;\n}\nfunction readOwnString(record, key) {\n try {\n const descriptor = Reflect.getOwnPropertyDescriptor(record, key);\n return descriptor && !descriptor.get && !descriptor.set && "value" in descriptor && typeof descriptor.value === "string" ? descriptor.value : void 0;\n } catch {\n return void 0;\n }\n}\nfunction headMetaSingletonKeyFromRecord(meta) {\n if (readOwnString(meta, "charset") !== void 0) return "meta:charset";\n const key = normalizeHeadIdentityValue(\n readOwnString(meta, "property") ?? readOwnString(meta, "name")\n );\n if (!key) return void 0;\n if (key === "theme-color") {\n return `meta:theme-color:${readOwnString(meta, "media")?.trim() ?? ""}`;\n }\n return SINGLETON_META_KEYS.has(key) ? `meta:${key}` : void 0;\n}\nfunction headLinkSingletonKeyFromRecord(link) {\n const rel = normalizeHeadIdentityValue(readOwnString(link, "rel"));\n return rel && SINGLETON_LINK_RELS.has(rel) ? `link:${rel}` : void 0;\n}\nfunction normalizeManagedHeadString(value) {\n return value.replace(/\\r\\n?/g, "\\n");\n}\nfunction inspectHeadProps(value) {\n if (typeof value !== "object" || value === null || Array.isArray(value)) return null;\n let prototype;\n let keys;\n try {\n prototype = Object.getPrototypeOf(value);\n keys = Reflect.ownKeys(value);\n } catch {\n return null;\n }\n if (prototype !== Object.prototype && prototype !== null) return null;\n const inspected = /* @__PURE__ */ new Map();\n let entries = 0;\n for (const key of keys) {\n let descriptor;\n try {\n descriptor = Reflect.getOwnPropertyDescriptor(value, key);\n } catch {\n return null;\n }\n if (!descriptor) return null;\n if (!descriptor.enumerable) continue;\n if (typeof key !== "string" || descriptor.get || descriptor.set || !("value" in descriptor)) {\n return null;\n }\n entries++;\n if (entries > MAX_HEAD_PROP_ENTRIES) return null;\n inspected.set(key, descriptor.value);\n }\n return inspected;\n}\nfunction normalizeContentPrimitive(value) {\n if (value === null || value === void 0 || typeof value === "boolean") return void 0;\n if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint") {\n return null;\n }\n const content = normalizeManagedHeadString(String(value));\n return headTextEncoder.encode(content).byteLength <= MAX_HEAD_CONTENT_BYTES ? content : null;\n}\nfunction normalizeManagedHeadAttributesFromProps(tagName, props, ambientNonce, excludedKeys = /* @__PURE__ */ new Set()) {\n const attributeMap = /* @__PURE__ */ new Map();\n for (const [key, value] of props) {\n if (key === "children" || key === "dangerouslySetInnerHTML" || excludedKeys.has(key)) {\n continue;\n }\n if (/^on/i.test(key) || typeof value === "function" || typeof value === "symbol" || typeof value === "object") {\n continue;\n }\n const name = (REACT_HEAD_ATTRIBUTE_NAMES[key] ?? key).toLowerCase();\n if (isHeadFrameworkAttribute(name) || !HEAD_ATTRIBUTE_NAME_PATTERN.test(name) || headTextEncoder.encode(name).byteLength > MAX_HEAD_ATTRIBUTE_NAME_BYTES) {\n continue;\n }\n if (BOOLEAN_HEAD_ATTRIBUTES.has(name)) {\n if (value !== false && value !== void 0) attributeMap.set(name, "");\n continue;\n }\n if (typeof value === "boolean") {\n if (name.startsWith("data-") || name.startsWith("aria-")) {\n attributeMap.set(name, String(value));\n }\n continue;\n }\n if (value === void 0) continue;\n if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint") {\n continue;\n }\n const normalizedValue = normalizeManagedHeadString(String(value));\n if (headTextEncoder.encode(normalizedValue).byteLength > MAX_HEAD_ATTRIBUTE_VALUE_BYTES) {\n return null;\n }\n attributeMap.set(name, normalizedValue);\n }\n if (tagName === "script" || tagName === "style") {\n attributeMap.delete("nonce");\n }\n const acceptsAmbientNonce = tagName === "style" || tagName === "script" && !attributeMap.has("src");\n if (acceptsAmbientNonce && ambientNonce) {\n const nonce = normalizeManagedHeadString(ambientNonce);\n if (headTextEncoder.encode(nonce).byteLength > MAX_HEAD_ATTRIBUTE_VALUE_BYTES) return null;\n attributeMap.set("nonce", nonce);\n }\n if (tagName === "link" && attributeMap.get("rel")?.trim().toLowerCase() === "preload" && attributeMap.get("as")?.trim().toLowerCase() === "font" && !attributeMap.has("crossorigin")) {\n attributeMap.set("crossorigin", "anonymous");\n }\n if (attributeMap.size > MAX_HEAD_PROP_ENTRIES) return null;\n let totalBytes = 0;\n for (const [name, value] of attributeMap) {\n totalBytes += headTextEncoder.encode(name).byteLength + headTextEncoder.encode(value).byteLength;\n if (totalBytes > MAX_HEAD_ATTRIBUTE_BYTES) return null;\n }\n return [...attributeMap.entries()].sort(([left], [right]) => left.localeCompare(right));\n}\nfunction singletonKey(tagName, attributes) {\n if (tagName === "title") return "title";\n const record = Object.fromEntries(attributes);\n if (tagName === "meta") return headMetaSingletonKeyFromRecord(record);\n if (tagName === "link") return headLinkSingletonKeyFromRecord(record);\n return void 0;\n}\nfunction scriptKeys(tagName, attributes) {\n if (tagName !== "script") return [];\n const keys = [];\n const id = attributes.get("id");\n const src = attributes.get("src");\n if (id) keys.push(`script:id:${id}`);\n if (src) keys.push(`script:src:${src}`);\n return keys;\n}\nfunction declaresDocumentEncoding(attributes) {\n return attributes.has("charset") || attributes.get("http-equiv")?.trim().toLowerCase() === "content-type";\n}\nfunction createManagedHeadDescriptor(tagName, attributes, content, contentMode) {\n const attributeMap = new Map(attributes);\n return {\n tagName,\n attributes,\n ...content !== void 0 && { content },\n contentMode,\n signature: JSON.stringify([\n tagName,\n attributes,\n contentMode,\n content ?? null\n ]),\n singletonKey: singletonKey(tagName, attributeMap),\n scriptKeys: scriptKeys(tagName, attributeMap)\n };\n}\nfunction descriptorFromManagedHeadRecord(rawTagName, record, options = {}) {\n const tagName = rawTagName.toLowerCase();\n if (!SUPPORTED_MANAGED_HEAD_TAGS.has(tagName)) return null;\n const inspected = inspectHeadProps(record);\n if (!inspected) return null;\n const excludedKeys = options.contentProperty ? /* @__PURE__ */ new Set([options.contentProperty]) : /* @__PURE__ */ new Set();\n const attributes = normalizeManagedHeadAttributesFromProps(\n tagName,\n inspected,\n options.ambientNonce,\n excludedKeys\n );\n if (!attributes) return null;\n const attributeMap = new Map(attributes);\n if (tagName === "meta" && declaresDocumentEncoding(attributeMap)) return null;\n if ((tagName === "meta" || tagName === "link") && attributes.length === 0) return null;\n let content;\n if (options.contentProperty) {\n const normalized = normalizeContentPrimitive(inspected.get(options.contentProperty));\n if (normalized === null) return null;\n content = normalized;\n }\n return createManagedHeadDescriptor(tagName, attributes, content, "text");\n}\nfunction headScriptKeysIntersect(left, right) {\n if (left.length === 0 || right.length === 0) return false;\n const rightKeys = new Set(right);\n return left.some((key) => rightKeys.has(key));\n}\nfunction aggregateManagedHeadDescriptors(descriptors) {\n const aggregated = [];\n const singletonIndexes = /* @__PURE__ */ new Map();\n const scriptKeysSeen = /* @__PURE__ */ new Set();\n for (const descriptor of descriptors) {\n if (descriptor.singletonKey) {\n const index = singletonIndexes.get(descriptor.singletonKey);\n if (index !== void 0) {\n aggregated[index] = descriptor;\n continue;\n }\n singletonIndexes.set(descriptor.singletonKey, aggregated.length);\n } else if (descriptor.scriptKeys.length > 0) {\n if (descriptor.scriptKeys.some((key) => scriptKeysSeen.has(key))) continue;\n for (const key of descriptor.scriptKeys) scriptKeysSeen.add(key);\n }\n aggregated.push(descriptor);\n }\n return aggregated;\n}\nfunction managedHeadDescriptorBytes(descriptor) {\n let bytes = headTextEncoder.encode(descriptor.tagName).byteLength;\n for (const [name, value] of descriptor.attributes) {\n bytes += headTextEncoder.encode(name).byteLength;\n bytes += headTextEncoder.encode(value).byteLength;\n }\n if (descriptor.content !== void 0) {\n bytes += headTextEncoder.encode(descriptor.content).byteLength;\n }\n return bytes;\n}\nfunction assertManagedHeadDescriptorBudget(descriptors) {\n if (descriptors.length > MAX_MANAGED_HEAD_ENTRIES) {\n throw new TypeError(\n `Managed head exceeds the ${MAX_MANAGED_HEAD_ENTRIES}-entry request limit`\n );\n }\n let bytes = 0;\n for (const descriptor of descriptors) {\n bytes += managedHeadDescriptorBytes(descriptor);\n if (bytes > MAX_MANAGED_HEAD_BYTES) {\n throw new TypeError(\n `Managed head exceeds the ${MAX_MANAGED_HEAD_BYTES}-byte request limit`\n );\n }\n }\n}\nfunction managedHeadDescriptorToTransportEntry(descriptor) {\n const attributes = descriptor.attributes.filter(([name]) => name !== "nonce");\n return {\n tagName: descriptor.tagName,\n attributes: attributes.map(([name, value]) => [name, value]),\n ...descriptor.content !== void 0 && { content: descriptor.content }\n };\n}\nfunction ownTransportValue(record, key) {\n let descriptor;\n try {\n descriptor = Reflect.getOwnPropertyDescriptor(record, key);\n } catch {\n return void 0;\n }\n if (!descriptor || descriptor.get || descriptor.set || !("value" in descriptor)) {\n return void 0;\n }\n return descriptor.value;\n}\nfunction descriptorFromManagedHeadTransportEntry(entry, ambientNonce) {\n if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {\n throw new TypeError("Managed-head transport entries must be plain objects");\n }\n let prototype;\n try {\n prototype = Object.getPrototypeOf(entry);\n } catch {\n throw new TypeError("Managed-head transport entry cannot be inspected");\n }\n if (prototype !== Object.prototype && prototype !== null) {\n throw new TypeError("Managed-head transport entries must be plain objects");\n }\n const tagName = ownTransportValue(entry, "tagName");\n const rawAttributes = ownTransportValue(entry, "attributes");\n const content = ownTransportValue(entry, "content");\n if (typeof tagName !== "string" || tagName !== tagName.toLowerCase() || !Array.isArray(rawAttributes)) {\n throw new TypeError("Managed-head transport entry is not canonical");\n }\n if (rawAttributes.length > MAX_HEAD_PROP_ENTRIES) {\n throw new TypeError("Managed-head transport entry exceeds the attribute limit");\n }\n if (content !== void 0 && typeof content !== "string") {\n throw new TypeError("Managed-head transport content must be a string");\n }\n const supportsText = tagName === "title" || tagName === "script" || tagName === "style";\n if (!supportsText && content !== void 0) {\n throw new TypeError("Managed-head transport content is invalid for this tag");\n }\n const record = /* @__PURE__ */ Object.create(null);\n const inputAttributes = [];\n const names = /* @__PURE__ */ new Set();\n for (let index = 0; index < rawAttributes.length; index += 1) {\n const pair = ownTransportValue(rawAttributes, String(index));\n if (!Array.isArray(pair) || pair.length !== 2) {\n throw new TypeError("Managed-head transport attributes must be string pairs");\n }\n const name = ownTransportValue(pair, "0");\n const value = ownTransportValue(pair, "1");\n if (typeof name !== "string" || typeof value !== "string") {\n throw new TypeError("Managed-head transport attributes must be string pairs");\n }\n const normalizedName = name.toLowerCase();\n if (name !== normalizedName || normalizedName === "nonce" || names.has(normalizedName)) {\n throw new TypeError("Managed-head transport attributes are not canonical");\n }\n names.add(normalizedName);\n inputAttributes.push([normalizedName, value]);\n Object.defineProperty(record, normalizedName, {\n enumerable: true,\n value\n });\n }\n if (content !== void 0) {\n Object.defineProperty(record, "__veryfront_transport_content", {\n enumerable: true,\n value: content\n });\n }\n const descriptor = descriptorFromManagedHeadRecord(tagName, record, {\n ...supportsText && { contentProperty: "__veryfront_transport_content" },\n ...(tagName === "script" || tagName === "style") && ambientNonce ? { ambientNonce } : {}\n });\n const normalizedInput = inputAttributes.sort(([left], [right]) => left.localeCompare(right));\n const normalizedOutput = descriptor?.attributes.filter(([name]) => name !== "nonce");\n if (!descriptor || JSON.stringify(normalizedOutput) !== JSON.stringify(normalizedInput) || supportsText && (descriptor.content ?? "") !== (content ?? "")) {\n throw new TypeError("Managed-head transport entry failed validation");\n }\n return descriptor;\n}\nvar BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";\nfunction decodeBase64Url(value) {\n if (value.length % 4 === 1 || !/^[A-Za-z0-9_-]*$/.test(value)) {\n throw new TypeError("Managed-head payload is not valid base64url");\n }\n const estimatedBytes = Math.floor(value.length * 3 / 4);\n if (estimatedBytes > MAX_MANAGED_HEAD_PAYLOAD_BYTES) {\n throw new TypeError("Managed-head payload exceeds its encoded size limit");\n }\n const bytes = new Uint8Array(estimatedBytes);\n let outputIndex = 0;\n let buffer = 0;\n let bits = 0;\n for (const character of value) {\n const decoded = BASE64URL_ALPHABET.indexOf(character);\n if (decoded < 0) throw new TypeError("Managed-head payload is not valid base64url");\n buffer = buffer << 6 | decoded;\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n bytes[outputIndex++] = buffer >> bits & 255;\n buffer &= bits === 0 ? 0 : (1 << bits) - 1;\n }\n }\n if (bits > 0 && buffer !== 0) {\n throw new TypeError("Managed-head payload has non-canonical trailing bits");\n }\n return bytes.subarray(0, outputIndex);\n}\nfunction inspectManagedHeadPayload(payload, ambientNonce) {\n if (typeof payload !== "string") throw new TypeError("Managed-head payload must be a string");\n const payloadBytes = headTextEncoder.encode(payload).byteLength;\n if (payloadBytes > MAX_MANAGED_HEAD_PAYLOAD_BYTES) {\n throw new TypeError("Managed-head payload exceeds its encoded size limit");\n }\n let decoded;\n try {\n decoded = new TextDecoder("utf-8", { fatal: true }).decode(decodeBase64Url(payload));\n } catch (error) {\n if (error instanceof TypeError) throw error;\n throw new TypeError("Managed-head payload is not valid UTF-8", { cause: error });\n }\n let entries;\n try {\n entries = JSON.parse(decoded);\n } catch (error) {\n throw new TypeError("Managed-head payload is not valid JSON", { cause: error });\n }\n if (!Array.isArray(entries) || entries.length > MAX_MANAGED_HEAD_ENTRIES) {\n throw new TypeError("Managed-head payload exceeds the entry limit");\n }\n const rawDescriptors = entries.map(\n (entry) => descriptorFromManagedHeadTransportEntry(entry, ambientNonce)\n );\n assertManagedHeadDescriptorBudget(rawDescriptors);\n return {\n descriptors: aggregateManagedHeadDescriptors(rawDescriptors),\n entryCount: rawDescriptors.length,\n descriptorBytes: rawDescriptors.reduce(\n (total, descriptor) => total + managedHeadDescriptorBytes(descriptor),\n 0\n ),\n payloadBytes\n };\n}\nfunction deserializeManagedHeadPayload(payload, ambientNonce) {\n return inspectManagedHeadPayload(payload, ambientNonce).descriptors;\n}\n\n// src/html/client-head-manager.ts\nvar HEAD_MANAGER_STATE_SYMBOL = /* @__PURE__ */ Symbol.for(\n "veryfront.client-head-manager.v2"\n);\nvar CROSS_PAGE_PRESERVED_SINGLETON_KEYS = /* @__PURE__ */ new Set([\n "meta:viewport",\n "link:manifest"\n]);\nfunction getClientHeadManagerState() {\n const globalState = globalThis;\n return globalState[HEAD_MANAGER_STATE_SYMBOL] ?? (globalState[HEAD_MANAGER_STATE_SYMBOL] = {\n documents: /* @__PURE__ */ new WeakMap()\n });\n}\nfunction getManagedHeadNonce(targetDocument) {\n if (typeof targetDocument.querySelector !== "function") return void 0;\n const element = targetDocument.querySelector(\n "script[nonce], style[nonce], link[nonce]"\n );\n if (!element) return void 0;\n const nonce = element.nonce || element.getAttribute("nonce") || "";\n return nonce || void 0;\n}\nfunction readElementAttributes(element) {\n const attributes = [];\n for (const attribute of element.attributes) {\n const name = attribute.name.toLowerCase();\n if (isHeadFrameworkAttribute(name)) continue;\n const nonce = name === "nonce" && "nonce" in element ? element.nonce : "";\n const value = BOOLEAN_HEAD_ATTRIBUTES.has(name) ? "" : nonce || attribute.value;\n attributes.push([name, value]);\n }\n return attributes.sort(([left], [right]) => left.localeCompare(right));\n}\nfunction elementSingletonKey(element) {\n const tagName = element.tagName.toLowerCase();\n if (tagName === "title") return "title";\n const attributes = Object.fromEntries(readElementAttributes(element));\n if (tagName === "meta") return headMetaSingletonKeyFromRecord(attributes);\n if (tagName === "link") return headLinkSingletonKeyFromRecord(attributes);\n return void 0;\n}\nfunction promoteToShellHeadBaseline(element) {\n for (const attribute of [...element.attributes]) {\n if (isHeadFrameworkAttribute(attribute.name)) {\n element.removeAttribute(attribute.name);\n }\n }\n element.setAttribute(HEAD_SHELL_PROVENANCE_ATTRIBUTE, "true");\n}\nfunction isCrossPagePreservedSingleton(element, singletonKey2 = elementSingletonKey(element)) {\n return element.parentElement !== null && singletonKey2 !== void 0 && CROSS_PAGE_PRESERVED_SINGLETON_KEYS.has(singletonKey2);\n}\nfunction isFrameworkOwnedHeadElement(element) {\n return element.getAttribute(HEAD_PROVENANCE_ATTRIBUTE) === "true" || element.getAttribute(HEAD_REACT_MANAGED_ATTRIBUTE) === "true" || element.getAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE) === "1" || element.getAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE) === "true" || element.getAttribute(HEAD_SHELL_PROVENANCE_ATTRIBUTE) === "true";\n}\nfunction retireFrameworkHeadElement(element) {\n if (isCrossPagePreservedSingleton(element)) {\n promoteToShellHeadBaseline(element);\n return;\n }\n element.remove();\n}\nfunction retireClientHeadOwnership(targetDocument) {\n const manager = getClientHeadManagerState().documents.get(targetDocument);\n if (manager) {\n manager.retire();\n return;\n }\n for (const element of [...targetDocument.head?.children ?? []]) {\n if (isFrameworkOwnedHeadElement(element)) retireFrameworkHeadElement(element);\n }\n}\n\n// src/html/client-route-head.ts\nvar ROUTE_HEAD_CONTENT_PROPERTY = "__veryfront_route_head_content";\nfunction descriptorFromHeadElement(element) {\n const record = /* @__PURE__ */ Object.create(null);\n for (const { name, value } of element.attributes) {\n if (!isHeadFrameworkAttribute(name)) record[name] = value;\n }\n const tagName = element.tagName.toLowerCase();\n const supportsText = tagName === "title" || tagName === "script" || tagName === "style";\n if (supportsText) record[ROUTE_HEAD_CONTENT_PROPERTY] = element.textContent ?? "";\n return descriptorFromManagedHeadRecord(\n tagName,\n record,\n supportsText ? { contentProperty: ROUTE_HEAD_CONTENT_PROPERTY } : void 0\n );\n}\nfunction writeRouteDescriptor(element, descriptor) {\n for (const attribute of [...element.attributes]) element.removeAttribute(attribute.name);\n for (const [name, value] of descriptor.attributes) element.setAttribute(name, value);\n element.textContent = descriptor.content ?? "";\n element.setAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE, "1");\n element.setAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE, "true");\n}\nfunction prepareClientRouteHeadEntries(entries, targetDocument = document) {\n if (entries === void 0) return [];\n if (!Array.isArray(entries) || entries.length > MAX_MANAGED_HEAD_ENTRIES) {\n throw new TypeError("Route head payload exceeds the entry limit");\n }\n const descriptors = aggregateManagedHeadDescriptors(\n entries.map(\n (entry) => descriptorFromManagedHeadTransportEntry(entry, getManagedHeadNonce(targetDocument))\n )\n );\n assertManagedHeadDescriptorBudget(descriptors);\n return descriptors;\n}\nfunction applyPreparedClientRouteHeadDescriptors(descriptors, targetDocument = document) {\n for (const descriptor of descriptors) {\n const described = [...targetDocument.head.children].flatMap((element2) => {\n const current = descriptorFromHeadElement(element2);\n return current ? [{ element: element2, descriptor: current }] : [];\n });\n if (descriptor.singletonKey) {\n const matches = described.filter(\n ({ descriptor: current }) => current.singletonKey === descriptor.singletonKey\n );\n const directive = matches.find(\n ({ element: element2 }) => element2.getAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE) === "1"\n );\n if (directive) {\n continue;\n }\n const reusable = matches.find(\n ({ element: element2 }) => element2.getAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE) === "true" || element2.getAttribute(HEAD_SHELL_PROVENANCE_ATTRIBUTE) === "true"\n );\n if (reusable) {\n writeRouteDescriptor(reusable.element, descriptor);\n continue;\n }\n }\n if (described.some(\n ({ descriptor: current }) => current.signature === descriptor.signature || headScriptKeysIntersect(current.scriptKeys, descriptor.scriptKeys)\n )) {\n continue;\n }\n const element = targetDocument.createElement(descriptor.tagName);\n writeRouteDescriptor(element, descriptor);\n targetDocument.head.appendChild(element);\n }\n}\nfunction updateRouteTitle(title, targetDocument = document) {\n if (typeof title !== "string" || !title) return;\n const titles = [...targetDocument.head.querySelectorAll("title")];\n if (titles.some((element) => element.getAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE) === "1")) {\n return;\n }\n let titleElement = titles.find(\n (element) => element.getAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE) === "true"\n );\n for (const element of titles) {\n if (element !== titleElement) element.remove();\n }\n if (!titleElement) {\n titleElement = targetDocument.createElement("title");\n titleElement.setAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE, "true");\n targetDocument.head.appendChild(titleElement);\n }\n titleElement.textContent = title;\n}\nfunction updateRouteMetaTag(targetDocument, selector, attributeName, attributeValue, content) {\n const matches = [...targetDocument.head.querySelectorAll(selector)];\n if (matches.some((element) => element.getAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE) === "1")) {\n return;\n }\n let metaTag = matches.find(\n (element) => element.getAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE) === "true"\n );\n if (!metaTag) {\n metaTag = targetDocument.createElement("meta");\n metaTag.setAttribute(attributeName, attributeValue);\n metaTag.setAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE, "true");\n targetDocument.head.appendChild(metaTag);\n }\n metaTag.setAttribute("content", content);\n}\nfunction updateRouteMetaTags(metadata, targetDocument = document) {\n if (typeof metadata.description === "string" && metadata.description) {\n updateRouteMetaTag(\n targetDocument,\n \'meta[name="description"]\',\n "name",\n "description",\n metadata.description\n );\n }\n if (typeof metadata.ogTitle === "string" && metadata.ogTitle) {\n updateRouteMetaTag(\n targetDocument,\n \'meta[property="og:title"]\',\n "property",\n "og:title",\n metadata.ogTitle\n );\n }\n}\n\n// src/html/hydration-data-element.ts\nvar HYDRATION_DATA_ELEMENT_ID = "veryfront-hydration-data";\nfunction findServerHydrationDataElement(document2) {\n try {\n const matches = [...document2.querySelectorAll(`[id="${HYDRATION_DATA_ELEMENT_ID}"]`)];\n if (matches.length !== 1) return null;\n const body = document2.body;\n if (!body) return null;\n const element = matches[0];\n if (body.firstElementChild !== element && element.parentElement !== body) return null;\n if (element.tagName?.toLowerCase() !== "script") return null;\n if (element.getAttribute("type")?.trim().toLowerCase() !== "application/json") return null;\n return element;\n } catch {\n return null;\n }\n}\n\n// src/routing/client/dom-utils.ts\nvar logger2 = rendererLogger.component("veryfront");\nfunction isInternalLink(target) {\n const href = target.getAttribute("href");\n if (!href) return false;\n if (href.startsWith("http") || href.startsWith("mailto:") || href.startsWith("#")) return false;\n const linkTarget = target.getAttribute("target");\n if (linkTarget === "_blank" || target.hasAttribute("download")) return false;\n return true;\n}\nfunction findAnchorElement(element) {\n let current = element;\n while (current && current.tagName !== "A") {\n current = current.parentElement;\n }\n return current instanceof HTMLAnchorElement ? current : null;\n}\nfunction applyHeadDirectives(container) {\n const targetDocument = container.ownerDocument ?? document;\n const nodes = [...container.querySelectorAll(\'[data-veryfront-head="1"], vf-head\')].filter(\n (node) => typeof node.getAttribute !== "function" || node.getAttribute(HEAD_REACT_OWNER_ATTRIBUTE) !== "1"\n );\n if (!nodes.length) return;\n retireClientHeadOwnership(targetDocument);\n cleanManagedHeadTags(targetDocument);\n for (const wrapper of nodes) {\n const TemplateElement = targetDocument.defaultView?.HTMLTemplateElement ?? globalThis.HTMLTemplateElement;\n const contentSource = TemplateElement && wrapper instanceof TemplateElement ? wrapper.content : wrapper;\n processHeadWrapper(contentSource, targetDocument);\n wrapper.parentElement?.removeChild(wrapper);\n }\n}\nfunction cleanManagedHeadTags(targetDocument) {\n for (const element of targetDocument.head.querySelectorAll(\n `[${HEAD_LEGACY_MANAGED_ATTRIBUTE}="1"]`\n )) {\n element.parentElement?.removeChild(element);\n }\n}\nfunction processHeadWrapper(wrapper, targetDocument) {\n const ElementConstructor = targetDocument.defaultView?.Element ?? globalThis.Element;\n const activeNonce = getManagedHeadNonce(targetDocument);\n for (const node of wrapper.childNodes) {\n if (!ElementConstructor || !(node instanceof ElementConstructor)) continue;\n const tagName = node.tagName.toLowerCase();\n if (headSingletonKey(node) === "meta:charset") continue;\n const clone = targetDocument.createElement(tagName);\n for (const { name, value } of node.attributes) {\n if (name.toLowerCase() !== "nonce") clone.setAttribute(name, value);\n }\n if (activeNonce && (tagName === "script" || tagName === "style" || tagName === "link")) {\n clone.setAttribute("nonce", activeNonce);\n }\n if (node.textContent && !clone.hasAttribute("src")) {\n clone.textContent = node.textContent;\n }\n replaceExistingHeadSingleton(targetDocument, clone);\n clone.setAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE, "1");\n clone.setAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE, "true");\n targetDocument.head.appendChild(clone);\n }\n}\nfunction headSingletonKey(element) {\n const tagName = element.tagName.toLowerCase();\n if (tagName === "title") return "title";\n if (tagName !== "meta" && tagName !== "link") return void 0;\n const attributes = /* @__PURE__ */ Object.create(null);\n if (!element.attributes) return void 0;\n for (const { name, value } of element.attributes) attributes[name.toLowerCase()] = value;\n if (tagName === "meta" && attributes["http-equiv"]?.trim().toLowerCase() === "content-type") {\n return "meta:charset";\n }\n return tagName === "meta" ? headMetaSingletonKeyFromRecord(attributes) : headLinkSingletonKeyFromRecord(attributes);\n}\nfunction replaceExistingHeadSingleton(targetDocument, replacement) {\n const singletonKey2 = headSingletonKey(replacement);\n if (!singletonKey2 || singletonKey2 === "meta:charset") return;\n for (const existing of [...targetDocument.head?.children ?? []]) {\n if (headSingletonKey(existing) === singletonKey2) existing.remove();\n }\n}\nfunction manageFocus(container) {\n try {\n const focusElement = container.querySelector("[data-router-focus]") || container.querySelector("main") || container.querySelector("h1");\n focusElement?.focus?.({ preventScroll: true });\n } catch (error) {\n logger2.warn("focus management failed", error);\n }\n}\nfunction extractPageDataFromScript() {\n const pageDataScript = document.querySelector("script[data-veryfront-page]");\n if (!pageDataScript) return null;\n try {\n const content = pageDataScript.textContent;\n if (!content) {\n logger2.warn("Page data script has no content");\n return {};\n }\n return JSON.parse(content);\n } catch (error) {\n logger2.error("Failed to parse page data:", error);\n return null;\n }\n}\nfunction snapshotClientRouteHead(targetDocument = document) {\n const hydrationDataScript = findServerHydrationDataElement(targetDocument);\n if (!hydrationDataScript?.textContent) return [];\n try {\n const hydrationData = JSON.parse(hydrationDataScript.textContent);\n if (typeof hydrationData.managedHeadPayload !== "string") return [];\n const descriptors = deserializeManagedHeadPayload(\n hydrationData.managedHeadPayload\n );\n const aggregated = aggregateManagedHeadDescriptors(descriptors);\n assertManagedHeadDescriptorBudget(aggregated);\n return aggregated.map(managedHeadDescriptorToTransportEntry);\n } catch {\n return [];\n }\n}\nfunction routeRequiresDocumentNavigation(data) {\n return Boolean(\n data.requiresFullDocumentNavigation || data.managedHead?.some((entry) => entry.tagName === "script") || typeof data.html === "string" && / entry.tagName === "script") || typeof root.querySelector === "function" && root.querySelector("script")\n ) {\n pageData = { ...pageData, requiresFullDocumentNavigation: true };\n }\n return { content, pageData, managedHead, dependencyPinningCacheKey };\n}\n\n// src/rendering/client/browser-stubs/config.ts\nvar DEFAULT_PREFETCH_DELAY_MS = 100;\nvar PAGE_TRANSITION_DELAY_MS = 150;\n\n// src/routing/client/navigation-handlers.ts\nvar logger3 = rendererLogger.component("veryfront");\nvar MAX_SCROLL_POSITIONS = 100;\nvar NavigationHandlers = class {\n constructor(prefetchDelay = DEFAULT_PREFETCH_DELAY_MS, prefetchOptions = {}) {\n __publicField(this, "prefetchQueue", /* @__PURE__ */ new Set());\n __publicField(this, "pendingTimeouts", /* @__PURE__ */ new Map());\n __publicField(this, "scrollPositions", /* @__PURE__ */ new Map());\n __publicField(this, "isPopStateNav", false);\n __publicField(this, "prefetchDelay");\n __publicField(this, "prefetchOptions");\n this.prefetchDelay = prefetchDelay;\n this.prefetchOptions = prefetchOptions;\n }\n createClickHandler(callbacks) {\n return (event) => {\n if (!(event.target instanceof HTMLElement)) return;\n const anchor = findAnchorElement(event.target);\n if (!anchor || !isInternalLink(anchor)) return;\n const href = anchor.getAttribute("href");\n if (!href) return;\n event.preventDefault();\n callbacks.onNavigate(href);\n };\n }\n createPopStateHandler(callbacks) {\n return (_event) => {\n this.isPopStateNav = true;\n const { pathname, search, hash } = globalThis.location;\n callbacks.onNavigate(`${pathname}${search}${hash}`);\n };\n }\n createMouseOverHandler(callbacks) {\n return (event) => {\n if (!(event.target instanceof HTMLElement)) return;\n if (event.target.tagName !== "A") return;\n const href = event.target.getAttribute("href");\n if (!href || href.startsWith("http") || href.startsWith("#")) return;\n if (!this.shouldPrefetchOnHover(event.target)) return;\n if (this.prefetchQueue.has(href)) return;\n this.prefetchQueue.add(href);\n const timeoutId = setTimeout(() => {\n callbacks.onPrefetch(href);\n this.prefetchQueue.delete(href);\n this.pendingTimeouts.delete(href);\n }, this.prefetchDelay);\n this.pendingTimeouts.set(href, timeoutId);\n };\n }\n shouldPrefetchOnHover(target) {\n const prefetchAttribute = target.getAttribute("data-prefetch");\n if (prefetchAttribute === "false") return false;\n if (prefetchAttribute === "true") return true;\n return Boolean(this.prefetchOptions.hover);\n }\n saveScrollPosition(path) {\n try {\n if (this.scrollPositions.size >= MAX_SCROLL_POSITIONS) {\n const oldest = this.scrollPositions.keys().next().value;\n if (oldest) this.scrollPositions.delete(oldest);\n }\n const scrollY = globalThis.scrollY;\n if (typeof scrollY !== "number") {\n logger3.debug("No valid scrollY value available");\n this.scrollPositions.set(path, 0);\n return;\n }\n this.scrollPositions.set(path, scrollY);\n } catch (error) {\n logger3.warn("failed to record scroll position", error);\n }\n }\n getScrollPosition(path) {\n const position = this.scrollPositions.get(path);\n if (position === void 0) {\n logger3.debug(`No scroll position stored for ${path}`);\n return 0;\n }\n return position;\n }\n isPopState() {\n return this.isPopStateNav;\n }\n clearPopStateFlag() {\n this.isPopStateNav = false;\n }\n clear() {\n for (const timeoutId of this.pendingTimeouts.values()) clearTimeout(timeoutId);\n this.pendingTimeouts.clear();\n this.prefetchQueue.clear();\n this.scrollPositions.clear();\n this.isPopStateNav = false;\n }\n};\n\n// src/rendering/client/browser-stubs/error-registry.ts\nfunction createBrowserError(name, fallbackMessage) {\n return {\n create(options = {}) {\n const error = new Error(options.detail ?? fallbackMessage);\n error.name = name;\n Object.assign(error, {\n status: options.status,\n context: options.context\n });\n return error;\n }\n };\n}\nvar NETWORK_ERROR = createBrowserError("NetworkError", "Network request failed");\nvar SECURITY_VIOLATION = createBrowserError("SecurityViolation", "Security violation");\n\n// src/html/html-detection.ts\nfunction isFullHTMLDocument(content) {\n const trimmed = content.trim().toLowerCase();\n return trimmed.startsWith("");\n}\n\n// src/routing/client/page-loader.ts\nvar logger4 = rendererLogger.component("veryfront");\nvar MAX_CACHE_SIZE = 50;\nvar DEPENDENCY_PINNING_RESPONSE_HEADER = "x-veryfront-dependency-pins";\nfunction reloadBrowserDocument(url) {\n if (typeof globalThis.location !== "undefined") {\n globalThis.location.assign(url);\n }\n}\nfunction readDependencyPinningCacheKey(doc) {\n if (!doc) return "off";\n try {\n const hydrationDataElement = findServerHydrationDataElement(doc);\n if (!hydrationDataElement?.textContent) return "off";\n const hydrationData = JSON.parse(hydrationDataElement.textContent);\n return typeof hydrationData.dependencyPinningCacheKey === "string" && hydrationData.dependencyPinningCacheKey.startsWith("on:") ? hydrationData.dependencyPinningCacheKey : "off";\n } catch (error) {\n logger4.debug("Failed to read dependency snapshot from hydration data:", error);\n return "off";\n }\n}\nvar PageLoader = class {\n constructor(doc = typeof document === "undefined" ? void 0 : document, reloadDocument = reloadBrowserDocument) {\n __publicField(this, "cache", /* @__PURE__ */ new Map());\n __publicField(this, "spaCache", /* @__PURE__ */ new Map());\n __publicField(this, "pendingRequests", /* @__PURE__ */ new Map());\n __publicField(this, "pendingSpaRequests", /* @__PURE__ */ new Map());\n /**\n * A loader belongs to the dependency snapshot of the document that created it.\n * Keeping this immutable also prevents cached or in-flight route data from\n * crossing snapshot boundaries if the hydration element is later replaced.\n */\n __publicField(this, "dependencyPinningCacheKey");\n __publicField(this, "reloadDocument");\n __publicField(this, "snapshotRecoveryStarted", false);\n this.dependencyPinningCacheKey = readDependencyPinningCacheKey(doc);\n this.reloadDocument = reloadDocument;\n }\n evictIfFull(map) {\n if (map.size < MAX_CACHE_SIZE) return;\n const oldest = map.keys().next().value;\n if (oldest) map.delete(oldest);\n }\n getCached(path) {\n return this.cache.get(this.snapshotScopedPath(path));\n }\n isCached(path) {\n return this.cache.has(this.snapshotScopedPath(path));\n }\n setCache(path, data) {\n this.evictIfFull(this.cache);\n this.cache.set(this.snapshotScopedPath(path), data);\n }\n clearCache() {\n this.cache.clear();\n this.spaCache.clear();\n this.pendingRequests.clear();\n this.pendingSpaRequests.clear();\n }\n getSpaCached(path) {\n return this.spaCache.get(this.snapshotScopedPath(path));\n }\n isSpaDataCached(path) {\n return this.spaCache.has(this.snapshotScopedPath(path));\n }\n setSpaCache(path, data) {\n this.evictIfFull(this.spaCache);\n this.spaCache.set(this.snapshotScopedPath(path), data);\n }\n async fetchPageData(path, reloadOnSnapshotFailure = true) {\n try {\n return await this.tryFetchJSON(path) ?? await this.fetchAndParseHTML(path);\n } catch (error) {\n this.recoverSnapshotFailure(error, path, reloadOnSnapshotFailure);\n throw error;\n }\n }\n async tryFetchJSON(path) {\n let response;\n try {\n const navigationUrl = new URL(path, "http://veryfront.local");\n const dataPath = navigationUrl.pathname === "/" ? "/index" : navigationUrl.pathname;\n const endpoint = `/_veryfront/data${dataPath}.json${navigationUrl.search}`;\n response = await fetch(endpoint, {\n headers: this.navigationHeaders("client")\n });\n } catch (error) {\n logger4.debug(`JSON fetch failed for ${path}, falling back to HTML:`, error);\n return null;\n }\n if (response.status === 409) {\n this.failDependencySnapshot(\n path,\n `Dependency snapshot is unavailable for ${path}`\n );\n }\n if (!response.ok) return null;\n let data;\n try {\n data = await response.json();\n } catch (error) {\n logger4.debug(`JSON response was invalid for ${path}, falling back to HTML:`, error);\n return null;\n }\n this.assertDependencySnapshot(\n data.dependencyPinningCacheKey,\n path,\n "route data"\n );\n if (typeof data.html === "string" && isFullHTMLDocument(data.html)) {\n const parsed = parsePageDataFromHTML(data.html);\n this.assertDependencySnapshot(\n parsed.dependencyPinningCacheKey,\n path,\n "route data HTML body"\n );\n return {\n ...parsed.pageData,\n ...data,\n html: parsed.content,\n managedHead: parsed.managedHead\n };\n }\n return routeRequiresDocumentNavigation(data) ? { ...data, requiresFullDocumentNavigation: true } : data;\n }\n async fetchAndParseHTML(path) {\n const response = await fetch(path, {\n headers: this.navigationHeaders("client")\n });\n if (response.status === 409) {\n this.failDependencySnapshot(\n path,\n `Dependency snapshot is unavailable for ${path}`\n );\n }\n if (!response.ok) {\n throw NETWORK_ERROR.create({\n detail: `Failed to fetch ${path}`,\n status: response.status,\n context: { path }\n });\n }\n this.assertDependencySnapshot(\n response.headers.get(DEPENDENCY_PINNING_RESPONSE_HEADER),\n path,\n "HTML response"\n );\n const html = await response.text();\n const {\n content,\n pageData,\n managedHead,\n dependencyPinningCacheKey\n } = parsePageDataFromHTML(html);\n this.assertDependencySnapshot(\n dependencyPinningCacheKey,\n path,\n "HTML body"\n );\n return { ...pageData, html: content, managedHead };\n }\n loadPage(path) {\n return this.loadPageWithSnapshotRecovery(path, true);\n }\n loadPageWithSnapshotRecovery(path, reloadOnSnapshotFailure) {\n const cachedData = this.getCached(path);\n if (cachedData) {\n logger4.debug(`Loading ${path} from cache`);\n return Promise.resolve(cachedData);\n }\n const pendingKey = this.snapshotScopedPath(path);\n const pending = this.pendingRequests.get(pendingKey);\n if (pending) {\n logger4.debug(`Reusing pending request for ${path}`);\n return this.withSnapshotRecovery(\n pending,\n path,\n reloadOnSnapshotFailure\n );\n }\n logger4.debug(`Creating pending request for ${path}`);\n const request = this.createPendingRequest(pendingKey, this.pendingRequests, async () => {\n const data = await this.fetchPageData(path, false);\n this.setCache(path, data);\n return data;\n });\n return this.withSnapshotRecovery(\n request,\n path,\n reloadOnSnapshotFailure\n );\n }\n async prefetch(path) {\n if (this.isCached(path)) return;\n logger4.debug(`Prefetching ${path}`);\n try {\n await this.loadPageWithSnapshotRecovery(path, false);\n } catch (error) {\n logger4.warn(\n `[Veryfront] Failed to prefetch ${path}`,\n error instanceof Error ? error : new Error(String(error))\n );\n }\n }\n async fetchSpaPageData(path, reloadOnSnapshotFailure = true) {\n try {\n const navigationUrl = new URL(path, "http://veryfront.local");\n const normalizedPath = navigationUrl.pathname === "/" ? "index" : navigationUrl.pathname.replace(/^\\//, "");\n const endpoint = `/_veryfront/page-data/${normalizedPath}.json${navigationUrl.search}`;\n logger4.debug(`Fetching SPA page data from ${endpoint}`);\n const response = await fetch(endpoint, {\n headers: this.navigationHeaders("spa")\n });\n if (response.status === 409) {\n this.failDependencySnapshot(\n path,\n `Dependency snapshot is unavailable for SPA page data ${path}`\n );\n }\n if (!response.ok) {\n throw NETWORK_ERROR.create({\n detail: `Failed to fetch SPA page data for ${path}`,\n status: response.status,\n context: { path }\n });\n }\n const data = await response.json();\n this.assertDependencySnapshot(\n data.dependencyPinningCacheKey,\n path,\n "SPA page data"\n );\n return data;\n } catch (error) {\n this.recoverSnapshotFailure(error, path, reloadOnSnapshotFailure);\n throw error;\n }\n }\n loadSpaPageData(path) {\n return this.loadSpaPageDataWithSnapshotRecovery(path, true);\n }\n loadSpaPageDataWithSnapshotRecovery(path, reloadOnSnapshotFailure) {\n const cachedData = this.getSpaCached(path);\n if (cachedData) {\n logger4.debug(`Loading SPA data for ${path} from cache`);\n return Promise.resolve(cachedData);\n }\n const pendingKey = this.snapshotScopedPath(path);\n const pending = this.pendingSpaRequests.get(pendingKey);\n if (pending) {\n logger4.debug(`Reusing pending SPA request for ${path}`);\n return this.withSnapshotRecovery(\n pending,\n path,\n reloadOnSnapshotFailure\n );\n }\n logger4.debug(`Creating pending SPA request for ${path}`);\n const request = this.createPendingRequest(pendingKey, this.pendingSpaRequests, async () => {\n const data = await this.fetchSpaPageData(path, false);\n this.setSpaCache(path, data);\n return data;\n });\n return this.withSnapshotRecovery(\n request,\n path,\n reloadOnSnapshotFailure\n );\n }\n async prefetchSpaPageData(path) {\n if (this.isSpaDataCached(path)) return;\n logger4.debug(`Prefetching SPA page data for ${path}`);\n try {\n await this.loadSpaPageDataWithSnapshotRecovery(path, false);\n } catch (error) {\n logger4.warn(\n `[Veryfront] Failed to prefetch SPA data for ${path}`,\n error instanceof Error ? error : new Error(String(error))\n );\n }\n }\n createPendingRequest(path, pendingMap, fetcher) {\n const request = (async () => {\n try {\n return await fetcher();\n } finally {\n pendingMap.delete(path);\n }\n })();\n pendingMap.set(path, request);\n return request;\n }\n snapshotScopedPath(path) {\n return this.dependencyPinningCacheKey.startsWith("on:") ? `${this.dependencyPinningCacheKey}\\0${path}` : path;\n }\n navigationHeaders(type) {\n return {\n "X-Veryfront-Navigation": type,\n ...this.dependencyPinningCacheKey.startsWith("on:") ? {\n [DEPENDENCY_PINNING_RESPONSE_HEADER]: this.dependencyPinningCacheKey\n } : {}\n };\n }\n assertDependencySnapshot(actualCacheKey, path, source) {\n const expectedCacheKey = this.dependencyPinningCacheKey.startsWith("on:") ? this.dependencyPinningCacheKey : void 0;\n const normalizedActualCacheKey = typeof actualCacheKey === "string" ? actualCacheKey : void 0;\n const matches = expectedCacheKey ? normalizedActualCacheKey === expectedCacheKey : normalizedActualCacheKey === void 0 || normalizedActualCacheKey === "off";\n if (matches) return;\n this.failDependencySnapshot(\n path,\n `Dependency snapshot mismatch in ${source} for ${path}`\n );\n }\n failDependencySnapshot(path, detail) {\n throw NETWORK_ERROR.create({\n detail,\n status: 409,\n context: { path }\n });\n }\n withSnapshotRecovery(promise, path, reloadOnSnapshotFailure) {\n return promise.catch((error) => {\n this.recoverSnapshotFailure(error, path, reloadOnSnapshotFailure);\n throw error;\n });\n }\n recoverSnapshotFailure(error, path, reloadOnSnapshotFailure) {\n if (!reloadOnSnapshotFailure || typeof error !== "object" || error === null || error.status !== 409) {\n return;\n }\n if (this.snapshotRecoveryStarted) return;\n this.snapshotRecoveryStarted = true;\n try {\n this.reloadDocument(path);\n } catch (reloadError) {\n this.snapshotRecoveryStarted = false;\n logger4.warn(\n `[Veryfront] Failed to reload after dependency snapshot conflict for ${path}`,\n reloadError instanceof Error ? reloadError : new Error(String(reloadError))\n );\n }\n }\n};\n\n// src/security/client/html-sanitizer.ts\nvar SUSPICIOUS_PATTERN_SPECS = [\n { source: String.raw`]*>[\\s\\S]*?<\\/script>`, flags: "gi", name: "inline script" },\n { source: String.raw`javascript:`, flags: "gi", name: "javascript: URL" },\n { source: String.raw`\\bon\\w+\\s*=`, flags: "gi", name: "event handler attribute" },\n { source: String.raw`data:\\s*text\\/html`, flags: "gi", name: "data: HTML URL" }\n];\nfunction createSuspiciousPatterns() {\n return SUSPICIOUS_PATTERN_SPECS.map(({ source, flags, name }) => ({\n pattern: new RegExp(source, flags),\n name\n }));\n}\nfunction isDevMode() {\n const g = globalThis;\n return g.__VERYFRONT_DEV__ === true || g.Deno?.env?.get?.("VERYFRONT_ENV") === "development";\n}\nfunction validateTrustedHtml(html, options = {}) {\n const { allowInlineScripts = false, strict = false, warn = true } = options;\n for (const { pattern, name } of createSuspiciousPatterns()) {\n if (allowInlineScripts && name === "inline script") continue;\n pattern.lastIndex = 0;\n if (!pattern.test(html)) continue;\n if (warn) console.warn(`[Security] Suspicious ${name} detected in server HTML`);\n if (strict || !isDevMode()) {\n throw SECURITY_VIOLATION.create({ detail: `Potentially unsafe HTML: ${name} detected` });\n }\n }\n return html;\n}\n\n// src/routing/client/page-transition.ts\nvar logger5 = rendererLogger.component("veryfront");\nvar PageTransition = class {\n constructor(setupViewportPrefetch) {\n __publicField(this, "setupViewportPrefetch", setupViewportPrefetch);\n __publicField(this, "pendingTransitionTimeout");\n __publicField(this, "pendingRoot");\n }\n destroy() {\n this.cancelPendingTransition();\n }\n cancelPendingTransition() {\n if (this.pendingTransitionTimeout !== void 0) {\n clearTimeout(this.pendingTransitionTimeout);\n this.pendingTransitionTimeout = void 0;\n }\n if (this.pendingRoot) {\n this.pendingRoot.style.opacity = "1";\n this.pendingRoot = void 0;\n }\n }\n updatePage(data, isPopState, scrollY) {\n this.cancelPendingTransition();\n if (routeRequiresDocumentNavigation(data)) {\n throw new TypeError("Scripted routes require a full document navigation");\n }\n const rootElement = document.getElementById("root");\n const preparedHead = prepareClientRouteHeadEntries(data.managedHead, document);\n const retainedTitle = document.title;\n if (!rootElement || data.html === void 0) {\n retireClientHeadOwnership(document);\n applyPreparedClientRouteHeadDescriptors(preparedHead, document);\n this.updateDocumentMetadata(document, data, retainedTitle);\n return;\n }\n const trustedHtml = validateTrustedHtml(String(data.html));\n this.performTransition(\n rootElement,\n data,\n trustedHtml,\n preparedHead,\n retainedTitle,\n isPopState,\n scrollY\n );\n }\n updateDocumentMetadata(targetDocument, data, retainedTitle) {\n updateRouteTitle(data.frontmatter?.title || retainedTitle, targetDocument);\n updateRouteMetaTags(data.frontmatter ?? {}, targetDocument);\n }\n performTransition(rootElement, data, trustedHtml, preparedHead, retainedTitle, isPopState, scrollY) {\n rootElement.style.opacity = "0";\n this.pendingRoot = rootElement;\n this.pendingTransitionTimeout = setTimeout(() => {\n this.pendingTransitionTimeout = void 0;\n this.pendingRoot = void 0;\n try {\n retireClientHeadOwnership(rootElement.ownerDocument);\n rootElement.innerHTML = trustedHtml;\n applyHeadDirectives(rootElement);\n applyPreparedClientRouteHeadDescriptors(preparedHead, rootElement.ownerDocument);\n this.updateDocumentMetadata(rootElement.ownerDocument, data, retainedTitle);\n this.setupViewportPrefetch(rootElement);\n manageFocus(rootElement);\n this.handleScroll(isPopState, scrollY);\n } catch (error) {\n logger5.error("Route transition commit failed; reloading the document", error);\n globalThis.location?.reload();\n } finally {\n rootElement.style.opacity = "1";\n }\n }, PAGE_TRANSITION_DELAY_MS);\n }\n handleScroll(isPopState, scrollY) {\n try {\n globalThis.scrollTo(0, isPopState ? scrollY : 0);\n } catch (error) {\n logger5.warn("scroll handling failed", error);\n }\n }\n showError(error) {\n const rootElement = document.getElementById("root");\n if (!rootElement) return;\n const errorDiv = document.createElement("div");\n errorDiv.className = "veryfront-error-page";\n const heading = document.createElement("h1");\n heading.textContent = "Oops! Something went wrong";\n const message = document.createElement("p");\n message.textContent = error.message;\n const button = document.createElement("button");\n button.type = "button";\n button.textContent = "Reload Page";\n button.onclick = () => globalThis.location.reload();\n errorDiv.append(heading, message, button);\n retireClientHeadOwnership(rootElement.ownerDocument);\n rootElement.innerHTML = "";\n rootElement.appendChild(errorDiv);\n }\n setLoadingState(loading) {\n const indicator = document.getElementById("veryfront-loading");\n if (indicator) indicator.style.display = loading ? "block" : "none";\n document.body.classList.toggle("veryfront-loading", loading);\n }\n};\n\n// src/routing/client/viewport-prefetch.ts\nvar logger6 = rendererLogger.component("veryfront");\nvar ViewportPrefetch = class {\n constructor(prefetchCallback, prefetchOptions = {}) {\n __publicField(this, "observer", null);\n __publicField(this, "prefetchCallback");\n __publicField(this, "prefetchOptions");\n this.prefetchCallback = prefetchCallback;\n this.prefetchOptions = prefetchOptions;\n }\n setup(root) {\n try {\n if (!("IntersectionObserver" in globalThis)) return;\n this.observer?.disconnect();\n this.createObserver();\n this.observeLinks(root);\n } catch (error) {\n logger6.debug("setupViewportPrefetch failed", error);\n }\n }\n createObserver() {\n this.observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n if (!(entry.target instanceof HTMLAnchorElement)) continue;\n const href = entry.target.getAttribute("href");\n if (href) this.prefetchCallback(href);\n this.observer?.unobserve(entry.target);\n }\n },\n { rootMargin: "200px" }\n );\n }\n observeLinks(root) {\n const anchors = root.querySelectorAll(\'a[href]:not([target="_blank"])\');\n const isViewportEnabled = Boolean(this.prefetchOptions.viewport);\n for (const anchor of anchors) {\n if (!this.shouldObserveAnchor(anchor, isViewportEnabled)) continue;\n this.observer?.observe(anchor);\n }\n }\n shouldObserveAnchor(anchor, isViewportEnabled) {\n const href = anchor.getAttribute("href");\n if (!href) return false;\n if (href.startsWith("http") || href.startsWith("#")) return false;\n if (anchor.getAttribute("download")) return false;\n const prefetchAttribute = anchor.getAttribute("data-prefetch");\n if (prefetchAttribute === "false") return false;\n return prefetchAttribute === "viewport" || isViewportEnabled;\n }\n disconnect() {\n if (!this.observer) return;\n try {\n this.observer.disconnect();\n } catch (error) {\n logger6.warn("prefetchObserver.disconnect failed", error);\n } finally {\n this.observer = null;\n }\n }\n};\n\n// src/rendering/client/router.ts\nvar logger7 = rendererLogger.component("veryfront");\nfunction toHistoryMode(options) {\n if (typeof options === "boolean") return options ? "push" : "none";\n return options?.history ?? "push";\n}\nvar VeryfrontRouter = class {\n constructor(options = {}) {\n __publicField(this, "baseUrl");\n __publicField(this, "currentPath");\n __publicField(this, "root", null);\n __publicField(this, "options");\n __publicField(this, "spaMode");\n __publicField(this, "spaNavigationHandler", null);\n __publicField(this, "navigationSequence", 0);\n __publicField(this, "pageLoader");\n __publicField(this, "navigationHandlers");\n __publicField(this, "pageTransition");\n __publicField(this, "viewportPrefetch");\n __publicField(this, "handleClick");\n __publicField(this, "handlePopState");\n __publicField(this, "handleMouseOver");\n const globalOptions = this.loadGlobalOptions();\n this.options = { ...globalOptions, ...options };\n this.baseUrl = this.options.baseUrl || globalThis.location.origin;\n this.currentPath = `${globalThis.location.pathname}${globalThis.location.search}${globalThis.location.hash}`;\n this.spaMode = this.options.spaMode ?? globalThis.__VERYFRONT_SPA_MODE__ ?? false;\n this.pageLoader = new PageLoader();\n this.navigationHandlers = new NavigationHandlers(\n this.options.prefetchDelay,\n this.options.prefetch\n );\n this.pageTransition = new PageTransition((root) => this.viewportPrefetch.setup(root));\n this.viewportPrefetch = new ViewportPrefetch(\n (path) => this.prefetch(path),\n this.options.prefetch\n );\n this.handleClick = this.navigationHandlers.createClickHandler({\n onNavigate: (url) => this.navigate(url),\n onPrefetch: (url) => this.prefetch(url)\n });\n this.handlePopState = this.navigationHandlers.createPopStateHandler({\n // The browser already updated the URL for a popstate, so don\'t touch history.\n onNavigate: (url) => this.navigate(url, { history: "none" }),\n onPrefetch: (url) => this.prefetch(url)\n });\n this.handleMouseOver = this.navigationHandlers.createMouseOverHandler({\n onNavigate: (url) => this.navigate(url),\n onPrefetch: (url) => this.prefetch(url)\n });\n getNavigationStore().setNavigator((href, options2) => this.navigate(href, options2));\n }\n registerNavigationHandler(handler) {\n logger7.debug("Registering SPA navigation handler");\n this.spaNavigationHandler = handler;\n this.spaMode = true;\n }\n /**\n * Notify React (and any other) subscribers that a navigation completed —\n * after full page loads, soft same-route changes, and popstate. Delegates to\n * the shared navigation store, the single subscription surface both bundles\n * share.\n */\n notify() {\n getNavigationStore().notify();\n }\n pathnameOf(url) {\n try {\n return new URL(url, this.baseUrl).pathname;\n } catch {\n return url.split("?")[0]?.split("#")[0] || this.currentPath;\n }\n }\n loadGlobalOptions() {\n try {\n const options = globalThis.__VERYFRONT_ROUTER_OPTS__;\n if (!options) {\n logger7.debug("No global options configured");\n return {};\n }\n return options;\n } catch (error) {\n logger7.error("Failed to read global options:", error);\n return {};\n }\n }\n init() {\n logger7.debug("Initializing client-side router");\n const rootElement = document.getElementById("root");\n if (!rootElement) {\n logger7.error("Root element not found");\n return;\n }\n const ReactDOMToUse = globalThis.ReactDOM ?? ReactDOM;\n this.root = ReactDOMToUse.createRoot(rootElement);\n document.addEventListener("click", this.handleClick);\n globalThis.addEventListener("popstate", this.handlePopState);\n document.addEventListener("mouseover", this.handleMouseOver);\n this.viewportPrefetch.setup(document);\n this.cacheCurrentPage();\n }\n cacheCurrentPage() {\n const pageData = extractPageDataFromScript();\n if (pageData) {\n const managedHead = snapshotClientRouteHead(document);\n this.pageLoader.setCache(this.currentPath, {\n ...pageData,\n managedHead,\n ...managedHead.some((entry) => entry.tagName === "script") || document.getElementById("root")?.querySelector("script") ? { requiresFullDocumentNavigation: true } : {}\n });\n }\n }\n /**\n * Navigate to a URL. `options` selects the history behaviour: `{ history:\n * "push" }` (default), `"replace"`, or `"none"` (the URL already reflects the\n * target, as after popstate). A boolean is accepted for backward\n * compatibility — `true` pushes, `false` maps to `"none"`.\n */\n async navigate(url, options) {\n logger7.debug(`Navigating to ${url} (SPA mode: ${this.spaMode})`);\n const navigationId = ++this.navigationSequence;\n this.pageTransition.cancelPendingTransition();\n this.pageTransition.setLoadingState(false);\n const history = toHistoryMode(options);\n const sameRoute = this.pathnameOf(url) === this.pathnameOf(this.currentPath);\n this.navigationHandlers.saveScrollPosition(this.currentPath);\n this.options.onStart?.(url);\n if (history === "replace") globalThis.history.replaceState({}, "", url);\n else if (history === "push") globalThis.history.pushState({}, "", url);\n if (sameRoute && !this.shouldRevalidate(url, sameRoute)) {\n if (!this.isCurrentNavigation(navigationId)) return;\n this.currentPath = url;\n this.notify();\n this.options.onComplete?.(url);\n this.options.onNavigate?.(url);\n return;\n }\n if (this.spaMode && this.spaNavigationHandler) {\n await this.loadSpaPage(url, navigationId);\n } else {\n if (await this.loadPage(url, true, navigationId)) return;\n }\n if (!this.isCurrentNavigation(navigationId)) return;\n this.notify();\n this.options.onNavigate?.(url);\n }\n isCurrentNavigation(navigationId) {\n return navigationId === this.navigationSequence;\n }\n /**\n * Whether a navigation should refetch page data. A route change always does;\n * a same-route (query/hash-only) change consults `options.shouldRevalidate`,\n * defaulting to `true` so server data is never shown stale.\n */\n shouldRevalidate(nextUrl, sameRoute) {\n const policy = this.options.shouldRevalidate;\n if (!policy) return true;\n return policy({ currentHref: this.currentPath, nextHref: nextUrl, sameRoute });\n }\n async loadSpaPage(path, navigationId) {\n logger7.debug(`Loading SPA page: ${path}`);\n try {\n const spaData = await this.pageLoader.loadSpaPageData(path);\n if (!this.isCurrentNavigation(navigationId)) return;\n await this.spaNavigationHandler?.(spaData);\n if (!this.isCurrentNavigation(navigationId)) return;\n this.currentPath = path;\n this.handleScrollAfterNavigation();\n this.options.onComplete?.(path);\n } catch (error) {\n if (!this.isCurrentNavigation(navigationId)) return;\n const normalizedError = error instanceof Error ? error : new Error(String(error));\n logger7.error(`Failed to load SPA page ${path}`, normalizedError);\n this.options.onError?.(normalizedError);\n this.pageTransition.showError(normalizedError);\n }\n }\n handleScrollAfterNavigation() {\n const isPopState = this.navigationHandlers.isPopState();\n const scrollY = this.navigationHandlers.getScrollPosition(this.currentPath);\n try {\n globalThis.scrollTo(0, isPopState ? scrollY : 0);\n } catch (error) {\n logger7.warn("scroll handling failed", error);\n }\n this.navigationHandlers.clearPopStateFlag();\n }\n /** Returns true when navigation was handed to the browser document loader. */\n async loadPage(path, updateUI = true, navigationId) {\n if (this.pageLoader.isCached(path)) {\n logger7.debug(`Loading ${path} from cache`);\n const data = this.pageLoader.getCached(path);\n if (data) {\n if (!this.isCurrentNavigation(navigationId)) return false;\n if (updateUI && data.requiresFullDocumentNavigation) {\n globalThis.location.assign(path);\n return true;\n }\n if (updateUI) this.updatePage(data, path);\n this.currentPath = path;\n this.pageTransition.setLoadingState(false);\n this.options.onComplete?.(path);\n return false;\n }\n logger7.warn(`Cache entry for ${path} was unexpectedly null, fetching fresh data`);\n }\n this.pageTransition.setLoadingState(true);\n try {\n const data = await this.pageLoader.loadPage(path);\n if (!this.isCurrentNavigation(navigationId)) return false;\n if (updateUI && data.requiresFullDocumentNavigation) {\n globalThis.location.assign(path);\n return true;\n }\n if (updateUI) this.updatePage(data, path);\n this.currentPath = path;\n this.options.onComplete?.(path);\n return false;\n } catch (error) {\n if (!this.isCurrentNavigation(navigationId)) return false;\n const normalizedError = error instanceof Error ? error : new Error(String(error));\n logger7.error(`Failed to load ${path}`, normalizedError);\n this.options.onError?.(normalizedError);\n this.pageTransition.showError(normalizedError);\n return false;\n } finally {\n if (this.isCurrentNavigation(navigationId)) this.pageTransition.setLoadingState(false);\n }\n }\n async prefetch(path) {\n if (this.spaMode) {\n await this.pageLoader.prefetchSpaPageData(path);\n return;\n }\n await this.pageLoader.prefetch(path);\n }\n updatePage(data, targetPath) {\n if (!this.root) return;\n const isPopState = this.navigationHandlers.isPopState();\n const scrollY = this.navigationHandlers.getScrollPosition(targetPath);\n this.pageTransition.updatePage(data, isPopState, scrollY);\n this.navigationHandlers.clearPopStateFlag();\n }\n destroy() {\n this.navigationSequence++;\n this.pageTransition.setLoadingState(false);\n document.removeEventListener("click", this.handleClick);\n globalThis.removeEventListener("popstate", this.handlePopState);\n document.removeEventListener("mouseover", this.handleMouseOver);\n this.viewportPrefetch.disconnect();\n this.pageLoader.clearCache();\n this.navigationHandlers.clear();\n this.pageTransition.destroy();\n }\n};\nfunction boot(options = {}) {\n if (typeof window === "undefined" || !globalThis.document) return null;\n const globalWithRouter = globalThis;\n if (globalWithRouter.veryFrontRouter) return globalWithRouter.veryFrontRouter;\n const { slug: _slug, ...routerOptions } = options;\n const router = new VeryfrontRouter(routerOptions);\n if (document.readyState === "loading") {\n document.addEventListener("DOMContentLoaded", () => router.init(), { once: true });\n } else {\n router.init();\n }\n globalWithRouter.veryFrontRouter = router;\n return router;\n}\nif (typeof window !== "undefined" && globalThis.document) {\n boot();\n}\nexport {\n VeryfrontRouter,\n boot\n};\n'; export const CLIENT_PREFETCH_BUNDLE: string | undefined = - 'var __defProp = Object.defineProperty;\nvar __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;\nvar __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);\n\n// src/rendering/client/browser-logger.ts\nvar ConditionalBrowserLogger = class {\n constructor(prefix, level) {\n __publicField(this, "prefix", prefix);\n __publicField(this, "level", level);\n }\n log(minLevel, fn, message, ...args) {\n if (this.level > minLevel) return;\n fn?.(message, ...args);\n }\n debug(message, ...args) {\n this.log(\n 0 /* DEBUG */,\n console.debug,\n `[${this.prefix}] DEBUG: ${message}`,\n ...args\n );\n }\n info(message, ...args) {\n this.log(1 /* INFO */, console.log, `[${this.prefix}] ${message}`, ...args);\n }\n warn(message, ...args) {\n this.log(\n 2 /* WARN */,\n console.warn,\n `[${this.prefix}] WARN: ${message}`,\n ...args\n );\n }\n error(message, ...args) {\n this.log(\n 3 /* ERROR */,\n console.error,\n `[${this.prefix}] ERROR: ${message}`,\n ...args\n );\n }\n};\nfunction getBrowserLogLevel() {\n if (typeof window === "undefined") return 2 /* WARN */;\n const g = globalThis;\n const isDevelopment = g.__VERYFRONT_DEV__ || g.__RSC_DEV__;\n if (!isDevelopment) return 2 /* WARN */;\n const isDebugEnabled2 = g.__VERYFRONT_DEBUG__ || g.__RSC_DEBUG__;\n return isDebugEnabled2 ? 0 /* DEBUG */ : 1 /* INFO */;\n}\nvar defaultLevel = getBrowserLogLevel();\nvar rscLogger = new ConditionalBrowserLogger("RSC", defaultLevel);\nvar prefetchLogger = new ConditionalBrowserLogger("PREFETCH", defaultLevel);\nvar hydrateLogger = new ConditionalBrowserLogger("HYDRATE", defaultLevel);\nvar browserLogger = new ConditionalBrowserLogger("VERYFRONT", defaultLevel);\n\n// src/rendering/client/prefetch/link-observer.ts\nfunction isAnchorElement(element) {\n return typeof HTMLAnchorElement !== "undefined" ? element instanceof HTMLAnchorElement : element.tagName === "A";\n}\nvar LinkObserver = class {\n constructor(options, prefetchedUrls) {\n __publicField(this, "options");\n __publicField(this, "intersectionObserver", null);\n __publicField(this, "mutationObserver", null);\n __publicField(this, "prefetchedUrls");\n __publicField(this, "pendingTimeouts", /* @__PURE__ */ new Map());\n __publicField(this, "elementTimeoutMap", /* @__PURE__ */ new WeakMap());\n __publicField(this, "timeoutCounter", 0);\n this.options = options;\n this.prefetchedUrls = prefetchedUrls;\n }\n init() {\n this.createIntersectionObserver();\n this.observeLinks();\n this.setupMutationObserver();\n }\n createIntersectionObserver() {\n this.intersectionObserver = new IntersectionObserver(\n (entries) => this.handleIntersection(entries),\n { rootMargin: this.options.rootMargin }\n );\n }\n handleIntersection(entries) {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n if (!isAnchorElement(entry.target)) continue;\n const link = entry.target;\n if (this.timeoutCounter > 1e6) this.timeoutCounter = 0;\n const timeoutKey = this.timeoutCounter++;\n const timeoutId = setTimeout(() => {\n this.pendingTimeouts.delete(timeoutKey);\n this.elementTimeoutMap.delete(link);\n this.options.onLinkVisible(link);\n }, this.options.delay);\n this.pendingTimeouts.set(timeoutKey, timeoutId);\n this.elementTimeoutMap.set(link, timeoutKey);\n }\n }\n observeLinks() {\n this.observeAnchors(document.querySelectorAll(\'a[href^="/"], a[href^="./"]\'));\n }\n setupMutationObserver() {\n this.mutationObserver = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n if (mutation.type !== "childList") continue;\n for (const node of mutation.addedNodes) {\n if (node.nodeType !== Node.ELEMENT_NODE) continue;\n this.observeElement(node);\n }\n for (const node of mutation.removedNodes) {\n if (node.nodeType !== Node.ELEMENT_NODE) continue;\n this.clearElementTimeouts(node);\n }\n }\n });\n this.mutationObserver.observe(document.body, { childList: true, subtree: true });\n }\n clearTimeoutForElement(element) {\n const timeoutKey = this.elementTimeoutMap.get(element);\n if (timeoutKey === void 0) return;\n const timeoutId = this.pendingTimeouts.get(timeoutKey);\n if (timeoutId !== void 0) {\n clearTimeout(timeoutId);\n this.pendingTimeouts.delete(timeoutKey);\n }\n this.elementTimeoutMap.delete(element);\n }\n clearElementTimeouts(element) {\n if (isAnchorElement(element)) this.clearTimeoutForElement(element);\n for (const link of element.querySelectorAll("a")) {\n this.clearTimeoutForElement(link);\n }\n }\n observeElement(element) {\n if (isAnchorElement(element) && this.isValidLink(element)) {\n this.intersectionObserver?.observe(element);\n }\n this.observeAnchors(element.querySelectorAll(\'a[href^="/"], a[href^="./"]\'));\n }\n observeAnchors(links) {\n for (const link of links) {\n if (!isAnchorElement(link)) continue;\n if (!this.isValidLink(link)) continue;\n this.intersectionObserver?.observe(link);\n }\n }\n isValidLink(link) {\n if (link.hostname !== globalThis.location.hostname) return false;\n if (link.hasAttribute("download")) return false;\n if (link.target === "_blank") return false;\n const url = link.href;\n if (this.prefetchedUrls.has(url)) return false;\n if (url === globalThis.location.href) return false;\n if (link.hash && link.pathname === globalThis.location.pathname) return false;\n if (link.dataset.noPrefetch) return false;\n return true;\n }\n destroy() {\n for (const timeoutId of this.pendingTimeouts.values()) {\n clearTimeout(timeoutId);\n }\n this.pendingTimeouts.clear();\n this.timeoutCounter = 0;\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = null;\n this.mutationObserver?.disconnect();\n this.mutationObserver = null;\n }\n};\n\n// src/rendering/client/prefetch/network-utils.ts\nvar NetworkUtils = class {\n constructor(allowedNetworks = ["4g", "wifi", "ethernet"]) {\n __publicField(this, "networkInfo");\n __publicField(this, "allowedNetworks");\n this.allowedNetworks = allowedNetworks;\n this.networkInfo = this.getNetworkConnection();\n }\n getNavigatorWithConnection() {\n if (typeof globalThis.navigator === "undefined") return null;\n return globalThis.navigator;\n }\n getNetworkConnection() {\n const nav = this.getNavigatorWithConnection();\n return nav?.connection ?? nav?.mozConnection ?? nav?.webkitConnection ?? null;\n }\n shouldPrefetch() {\n if (this.networkInfo?.saveData) return false;\n const effectiveType = this.networkInfo?.effectiveType;\n if (effectiveType != null && !this.allowedNetworks.includes(effectiveType)) return false;\n return true;\n }\n onNetworkChange(callback) {\n this.networkInfo?.addEventListener?.("change", callback);\n }\n getNetworkInfo() {\n return this.networkInfo;\n }\n};\n\n// src/utils/constants/css.ts\nvar MAX_CSS_FILE_BYTES = 16 * 1024 * 1024;\nvar MAX_CSS_TOTAL_BYTES = 64 * 1024 * 1024;\nvar MAX_CSS_OUTPUT_FILE_BYTES = 32 * 1024 * 1024;\n\n// src/utils/constants/buffers.ts\nvar DEFAULT_MAX_BODY_SIZE_BYTES = 1024 * 1024;\nvar DEFAULT_MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;\nvar PREFETCH_QUEUE_MAX_SIZE_BYTES = DEFAULT_MAX_BODY_SIZE_BYTES;\nvar MAX_BUNDLE_CHUNK_SIZE_BYTES = 4096 * 1024;\n\n// src/utils/constants/limits.ts\nvar MAX_TIMER_DELAY_MS = 2147483647;\n\n// src/utils/constants/cache.ts\nvar SECONDS_PER_MINUTE = 60;\nvar MINUTES_PER_HOUR = 60;\nvar HOURS_PER_DAY = 24;\nvar MS_PER_SECOND = 1e3;\nvar MS_PER_MINUTE = SECONDS_PER_MINUTE * MS_PER_SECOND;\nvar MS_PER_HOUR = MINUTES_PER_HOUR * MS_PER_MINUTE;\nvar ONE_DAY_MS = HOURS_PER_DAY * MS_PER_HOUR;\nfunction getEnvString(key) {\n const g = globalThis;\n try {\n return g.Deno?.env?.get?.(key) ?? g.process?.env?.[key];\n } catch (_) {\n return void 0;\n }\n}\nvar MAX_CONFIGURED_CACHE_ENTRIES = 1e6;\nvar MAX_CONFIGURED_CACHE_SIZE_MB = 64 * 1024;\nvar MAX_CONFIGURED_CONCURRENCY = 1e4;\nvar MAX_CONFIGURED_TTL_SECONDS = 365 * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE;\nvar BYTES_PER_MB = 1024 * 1024;\nvar MAX_CACHE_TTL_SECONDS = 2147483647;\nvar MAX_CACHE_TTL_MILLISECONDS = MAX_CACHE_TTL_SECONDS * MS_PER_SECOND;\nfunction getEnvInteger(key, fallback, { min = 1, max }) {\n const value = getEnvString(key);\n if (value == null) return fallback;\n const normalized = value.trim();\n if (!/^\\d+$/.test(normalized)) return fallback;\n const parsed = Number(normalized);\n if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) return fallback;\n return parsed;\n}\nfunction getStrictEnvInteger(key, fallback, { min = 1, max }) {\n const value = getEnvString(key);\n if (value == null) return fallback;\n const normalized = value.trim();\n if (!/^\\d+$/.test(normalized)) {\n throw new RangeError(\n `${key} must be a base-10 integer between ${min} and ${max}`\n );\n }\n const parsed = Number(normalized);\n if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {\n throw new RangeError(`${key} must be between ${min} and ${max}`);\n }\n return parsed;\n}\nfunction getEnvCacheEntries(key, fallback) {\n return getEnvInteger(key, fallback, { max: MAX_CONFIGURED_CACHE_ENTRIES });\n}\nfunction getEnvCacheSizeMb(key, fallback) {\n return getEnvInteger(key, fallback, { max: MAX_CONFIGURED_CACHE_SIZE_MB });\n}\nfunction getEnvTtlSeconds(key, fallback) {\n return getEnvInteger(key, fallback, { max: MAX_CONFIGURED_TTL_SECONDS });\n}\nvar DEFAULT_LRU_MAX_ENTRIES = getEnvCacheEntries("LRU_DEFAULT_MAX_ENTRIES", 100);\nvar COMPONENT_LOADER_MAX_ENTRIES = getEnvCacheEntries("COMPONENT_LOADER_MAX_ENTRIES", 200);\nvar COMPONENT_LOADER_TTL_MS = 10 * MS_PER_MINUTE;\nvar MDX_RENDERER_MAX_ENTRIES = getEnvCacheEntries("MDX_RENDERER_MAX_ENTRIES", 500);\nvar MDX_RENDERER_TTL_MS = 10 * MS_PER_MINUTE;\nvar RENDERER_CORE_MAX_ENTRIES = getEnvCacheEntries("RENDERER_CORE_MAX_ENTRIES", 200);\nvar RENDERER_CORE_TTL_MS = 5 * MS_PER_MINUTE;\nvar TSX_LAYOUT_MAX_ENTRIES = getEnvCacheEntries("TSX_LAYOUT_MAX_ENTRIES", 100);\nvar TSX_LAYOUT_TTL_MS = 10 * MS_PER_MINUTE;\nvar TSX_LAYOUT_PER_PROJECT_MAX_ENTRIES = getEnvCacheEntries(\n "TSX_LAYOUT_PER_PROJECT_MAX_ENTRIES",\n Math.ceil(TSX_LAYOUT_MAX_ENTRIES / 10)\n);\nvar DATA_FETCHING_MAX_ENTRIES = getStrictEnvInteger(\n "DATA_FETCHING_MAX_ENTRIES",\n 500,\n { max: MAX_CONFIGURED_CACHE_ENTRIES }\n);\nvar DATA_FETCHING_MAX_ENTRIES_PER_PROJECT = getStrictEnvInteger(\n "DATA_FETCHING_MAX_ENTRIES_PER_PROJECT",\n Math.max(1, Math.ceil(DATA_FETCHING_MAX_ENTRIES / 5)),\n { max: DATA_FETCHING_MAX_ENTRIES }\n);\nvar dataFetchingMaxSizeMb = getStrictEnvInteger(\n "DATA_FETCHING_MAX_SIZE_MB",\n 50,\n { max: MAX_CONFIGURED_CACHE_SIZE_MB }\n);\nvar DATA_FETCHING_MAX_SIZE_BYTES = dataFetchingMaxSizeMb * BYTES_PER_MB;\nvar DATA_FETCHING_MAX_SIZE_BYTES_PER_PROJECT = getStrictEnvInteger(\n "DATA_FETCHING_MAX_SIZE_MB_PER_PROJECT",\n Math.max(1, Math.ceil(dataFetchingMaxSizeMb / 5)),\n { max: dataFetchingMaxSizeMb }\n) * BYTES_PER_MB;\nvar DATA_FETCHING_TTL_MS = 10 * MS_PER_MINUTE;\nvar DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS = getStrictEnvInteger(\n "DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS",\n 512,\n { max: MAX_CONFIGURED_CONCURRENCY }\n);\nvar DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS_PER_PROJECT = getStrictEnvInteger(\n "DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS_PER_PROJECT",\n Math.min(128, DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS),\n { max: DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS }\n);\nvar MDX_CACHE_TTL_DEVELOPMENT_MS = 5 * MS_PER_MINUTE;\nvar BUNDLE_CACHE_TTL_DEVELOPMENT_MS = 5 * MS_PER_MINUTE;\nvar BUNDLE_MANIFEST_PROD_TTL_MS = 7 * ONE_DAY_MS;\nvar SERVER_ACTION_DEFAULT_TTL_SEC = MINUTES_PER_HOUR * SECONDS_PER_MINUTE;\nvar DISTRIBUTED_SSR_MODULE_TTL_PRODUCTION_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_SSR_MODULE_TTL_SEC",\n 6 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_SSR_MODULE_TTL_PREVIEW_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_SSR_MODULE_TTL_PREVIEW_SEC",\n 10 * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_TRANSFORM_TTL_PRODUCTION_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_TRANSFORM_TTL_SEC",\n 6 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_TRANSFORM_TTL_PREVIEW_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_TRANSFORM_TTL_PREVIEW_SEC",\n 10 * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_FILE_TTL_PRODUCTION_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_FILE_TTL_SEC",\n MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_FILE_TTL_PREVIEW_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_FILE_TTL_PREVIEW_SEC",\n 5 * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_CSS_TTL_PRODUCTION_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_CSS_TTL_SEC",\n 6 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_CSS_TTL_PREVIEW_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_CSS_TTL_PREVIEW_SEC",\n 10 * SECONDS_PER_MINUTE\n);\nvar LRU_DEFAULT_MAX_ENTRIES_V2 = getEnvCacheEntries("LRU_MAX_ENTRIES", 2e3);\nvar LRU_DEFAULT_MAX_SIZE_BYTES = getEnvCacheSizeMb("LRU_MAX_SIZE_MB", 200) * BYTES_PER_MB;\nvar MEMORY_CACHE_MAX_ENTRIES = getEnvCacheEntries("MEMORY_CACHE_MAX_ENTRIES", 2e3);\nvar MEMORY_CACHE_MAX_SIZE_BYTES = getEnvCacheSizeMb("MEMORY_CACHE_MAX_SIZE_MB", 50) * BYTES_PER_MB;\nvar FILE_CACHE_MAX_ENTRIES = getEnvCacheEntries("FILE_CACHE_MAX_ENTRIES", 1e3);\nvar FILE_CACHE_MAX_SIZE_MB = getEnvCacheSizeMb("FILE_CACHE_MAX_SIZE_MB", 100);\nvar MAX_CONCURRENT_REVALIDATIONS = getEnvInteger("MAX_CONCURRENT_REVALIDATIONS", 32, {\n max: MAX_CONFIGURED_CONCURRENCY\n});\nvar MAX_CONCURRENT_HTTP_FETCHES = getEnvInteger("MAX_CONCURRENT_HTTP_FETCHES", 50, {\n max: MAX_CONFIGURED_CONCURRENCY\n});\nvar REVALIDATION_TIMEOUT_MS = getEnvInteger("REVALIDATION_TIMEOUT_MS", 15e3, {\n max: MAX_TIMER_DELAY_MS\n});\nvar REVALIDATION_PER_PROJECT_LIMIT = getEnvInteger(\n "REVALIDATION_PER_PROJECT_LIMIT",\n Math.ceil(MAX_CONCURRENT_REVALIDATIONS / 3),\n { min: 0, max: MAX_CONFIGURED_CONCURRENCY }\n);\nvar BUNDLE_MANIFEST_DISTRIBUTED_TTL_SEC = getEnvTtlSeconds(\n "BUNDLE_MANIFEST_DISTRIBUTED_TTL_SEC",\n HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n // 24 hours (86400)\n);\nvar BUNDLE_MANIFEST_LRU_MAX_ENTRIES = getEnvCacheEntries(\n "BUNDLE_MANIFEST_LRU_MAX_ENTRIES",\n 5e3\n);\nvar BUNDLE_MANIFEST_MEMORY_MAX_METADATA_SIZE_BYTES = getEnvCacheSizeMb(\n "BUNDLE_MANIFEST_MEMORY_MAX_METADATA_SIZE_MB",\n 128\n) * BYTES_PER_MB;\nvar BUNDLE_MANIFEST_MEMORY_MAX_CODE_SIZE_BYTES = getEnvCacheSizeMb(\n "BUNDLE_MANIFEST_MEMORY_MAX_CODE_SIZE_MB",\n 256\n) * BYTES_PER_MB;\nvar HTTP_MODULE_CACHE_MAX_ENTRIES = getEnvCacheEntries(\n "HTTP_MODULE_CACHE_MAX_ENTRIES",\n 2e3\n);\nvar HTTP_MODULE_DISTRIBUTED_TTL_SEC = getEnvTtlSeconds(\n "HTTP_MODULE_DISTRIBUTED_TTL_SEC",\n HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n // 24 hours (86400)\n);\nvar TRANSFORM_DISTRIBUTED_TTL_SEC = getEnvTtlSeconds(\n "TRANSFORM_DISTRIBUTED_TTL_SEC",\n 6 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n // 6 hours (21600)\n);\nvar MODULE_CACHE_MAX_ENTRIES = getEnvCacheEntries("MODULE_CACHE_MAX_ENTRIES", 1e4);\nvar MODULE_CACHE_TTL_MS = getEnvInteger(\n "MODULE_CACHE_TTL_MS",\n 5 * MS_PER_MINUTE,\n // 5 minutes - short enough to pick up changes, long enough to cache\n { max: MAX_TIMER_DELAY_MS }\n);\nvar ESM_CACHE_MAX_ENTRIES = getEnvCacheEntries("ESM_CACHE_MAX_ENTRIES", 5e3);\nvar ESM_CACHE_TTL_MS = getEnvInteger(\n "ESM_CACHE_TTL_MS",\n 10 * MS_PER_MINUTE,\n // 10 minutes - external modules change less frequently\n { max: MAX_TIMER_DELAY_MS }\n);\n\n// src/utils/constants/http.ts\nvar KB_IN_BYTES = 1024;\nvar PREFETCH_MAX_SIZE_BYTES = 200 * KB_IN_BYTES;\n\n// src/utils/constants/hmr.ts\nvar HMR_MAX_MESSAGE_SIZE_BYTES = 1024 * KB_IN_BYTES;\n\n// src/utils/constants/network.ts\nvar BYTES_PER_KB = 1024;\nvar BYTES_PER_MB2 = BYTES_PER_KB * BYTES_PER_KB;\n\n// src/utils/constants/security.ts\nvar MAX_CSRF_TTL_SECONDS = Number.MAX_SAFE_INTEGER;\n\n// src/platform/compat/constants.ts\nvar DEFAULT_PORT = 3e3;\nvar LOCALHOST = Object.freeze(\n {\n IPV4: "127.0.0.1",\n IPV6: "::1",\n HOSTNAME: "localhost"\n }\n);\n\n// src/config/defaults.ts\nvar DEFAULT_TIMEOUT_MS = 5e3;\nvar SSR_TIMEOUT_MS = 1e4;\nvar SSR_MAX_BUFFERED_BYTES = 16 * 1024 * 1024;\nvar SANDBOX_TIMEOUT_MS = 5e3;\nvar DEFAULT_CACHE_MAX_SIZE = 100;\nvar DURATION_HISTOGRAM_BOUNDARIES_MS = Object.freeze(\n [\n 5,\n 10,\n 25,\n 50,\n 75,\n 100,\n 250,\n 500,\n 750,\n 1e3,\n 2500,\n 5e3,\n 7500,\n 1e4\n ]\n);\nvar SIZE_HISTOGRAM_BOUNDARIES_KB = Object.freeze(\n [\n 1,\n 5,\n 10,\n 25,\n 50,\n 100,\n 250,\n 500,\n 1e3,\n 2500,\n 5e3,\n 1e4\n ]\n);\nvar defaultConfig = Object.freeze(\n {\n server: Object.freeze({\n port: DEFAULT_PORT,\n hostname: "0.0.0.0"\n }),\n timeouts: Object.freeze({\n default: DEFAULT_TIMEOUT_MS,\n api: 3e4,\n ssr: SSR_TIMEOUT_MS,\n hmr: 3e4,\n sandbox: SANDBOX_TIMEOUT_MS\n }),\n cache: Object.freeze({\n jit: Object.freeze({\n maxSize: DEFAULT_CACHE_MAX_SIZE,\n tempDirPrefix: "vf-bundle-"\n })\n }),\n metrics: Object.freeze({\n ssrBoundaries: DURATION_HISTOGRAM_BOUNDARIES_MS\n })\n }\n);\n\n// src/utils/constants/server.ts\nvar INTERNAL_PREFIX = "/_veryfront";\nvar INTERNAL_PATH_PREFIXES = {\n /** React Server Components endpoints */\n RSC: `${INTERNAL_PREFIX}/rsc/`,\n /** File system access endpoints (base64 encoded paths) */\n FS: `${INTERNAL_PREFIX}/fs/`,\n /** Virtual module system */\n MODULES: `${INTERNAL_PREFIX}/modules/`,\n /** Generated page modules */\n PAGES: `${INTERNAL_PREFIX}/pages/`,\n /** Data JSON endpoints */\n DATA: `${INTERNAL_PREFIX}/data/`,\n /** Library modules and large vendor surfaces */\n LIB: `${INTERNAL_PREFIX}/lib/`,\n /** Chunk assets */\n CHUNKS: `${INTERNAL_PREFIX}/chunks/`,\n /** Client component modules */\n CLIENT: `${INTERNAL_PREFIX}/client/`\n};\nvar INTERNAL_ENDPOINTS = {\n // Development endpoints\n HMR_RUNTIME: `${INTERNAL_PREFIX}/hmr-runtime.js`,\n HMR: `${INTERNAL_PREFIX}/hmr.js`,\n ERROR_OVERLAY: `${INTERNAL_PREFIX}/error-overlay.js`,\n // Legacy endpoint retained for backward compatibility (no active handler).\n DEV_LOADER: `${INTERNAL_PREFIX}/dev-loader.js`,\n CLIENT_LOG: `${INTERNAL_PREFIX}/log`,\n // Production endpoints\n CLIENT_JS: `${INTERNAL_PREFIX}/client.js`,\n ROUTER_JS: `${INTERNAL_PREFIX}/router.js`,\n PREFETCH_JS: `${INTERNAL_PREFIX}/prefetch.js`,\n MANIFEST_JSON: `${INTERNAL_PREFIX}/manifest.json`,\n APP_JS: `${INTERNAL_PREFIX}/app.js`,\n // RSC endpoints\n RSC_CLIENT: `${INTERNAL_PREFIX}/rsc/client.js`,\n RSC_MANIFEST: `${INTERNAL_PREFIX}/rsc/manifest`,\n RSC_STREAM: `${INTERNAL_PREFIX}/rsc/stream`,\n RSC_PAYLOAD: `${INTERNAL_PREFIX}/rsc/payload`,\n RSC_RENDER: `${INTERNAL_PREFIX}/rsc/render`,\n RSC_PAGE: `${INTERNAL_PREFIX}/rsc/page`,\n RSC_MODULE: `${INTERNAL_PREFIX}/rsc/module`,\n RSC_DOM: `${INTERNAL_PREFIX}/rsc/dom.js`,\n // Library module endpoints\n LIB_CHAT_REACT: `${INTERNAL_PREFIX}/lib/chat/react.js`,\n LIB_CHAT_COMPONENTS: `${INTERNAL_PREFIX}/lib/chat/components.js`,\n LIB_CHAT_PRIMITIVES: `${INTERNAL_PREFIX}/lib/chat/primitives.js`\n};\nvar PROJECT_DIRS = {\n /** Base veryfront internal directory */\n ROOT: ".veryfront",\n /** Cache directory for build artifacts, transforms, etc. */\n CACHE: ".veryfront/cache",\n /** KV store directory */\n KV: ".veryfront/kv",\n /** Log files directory */\n LOGS: ".veryfront/logs",\n /** Temporary files directory */\n TMP: ".veryfront/tmp"\n};\nvar DEFAULT_CACHE_DIR = PROJECT_DIRS.CACHE;\nvar DEV_SERVER_ENDPOINTS = {\n HMR_RUNTIME: INTERNAL_ENDPOINTS.HMR_RUNTIME,\n ERROR_OVERLAY: INTERNAL_ENDPOINTS.ERROR_OVERLAY\n};\n\n// src/rendering/client/prefetch/prefetch-queue.ts\nvar DEFAULT_OPTIONS = {\n maxConcurrent: 4,\n maxSize: PREFETCH_QUEUE_MAX_SIZE_BYTES,\n timeout: 5e3\n};\nfunction isAbortError(error) {\n if (typeof error !== "object" || error === null) return false;\n if (!("name" in error)) return false;\n return error.name === "AbortError";\n}\nvar PrefetchQueue = class {\n constructor(options = {}, prefetchedUrls) {\n __publicField(this, "options");\n __publicField(this, "controllers", /* @__PURE__ */ new Map());\n __publicField(this, "prefetchedUrls");\n __publicField(this, "concurrent", 0);\n __publicField(this, "stopped", false);\n __publicField(this, "onResourcesFetched");\n this.options = { ...DEFAULT_OPTIONS, ...options };\n this.prefetchedUrls = prefetchedUrls ?? /* @__PURE__ */ new Set();\n }\n setResourceCallback(callback) {\n this.onResourcesFetched = callback;\n }\n enqueue(url) {\n void this.prefetch(url);\n }\n has(url) {\n return this.prefetchedUrls.has(url) || this.controllers.has(url);\n }\n get size() {\n return this.controllers.size;\n }\n clear() {\n this.stopAll();\n this.prefetchedUrls.clear();\n }\n start() {\n this.stopped = false;\n }\n stop() {\n this.stopped = true;\n this.stopAll();\n }\n getQueueSize() {\n return this.controllers.size;\n }\n getConcurrentCount() {\n return this.concurrent;\n }\n async prefetchLink(link) {\n if (this.stopped) return;\n const url = link.href;\n if (!url || this.controllers.has(url) || this.prefetchedUrls.has(url)) return;\n if (this.concurrent >= this.options.maxConcurrent) {\n prefetchLogger.debug?.(`Prefetch queue full, skipping ${url}`);\n return;\n }\n let parsedUrl;\n try {\n parsedUrl = new URL(url);\n } catch (_) {\n prefetchLogger.debug?.(`Invalid prefetch URL ${url}`);\n return;\n }\n const controller = new AbortController();\n this.controllers.set(url, controller);\n this.concurrent += 1;\n const timeoutId = this.options.timeout > 0 ? setTimeout(() => controller.abort(), this.options.timeout) : void 0;\n try {\n const response = await fetch(parsedUrl.toString(), {\n method: "GET",\n signal: controller.signal,\n headers: { "X-Veryfront-Prefetch": "1" }\n });\n if (!response.ok) return;\n if (this.isResponseTooLarge(response)) {\n prefetchLogger.debug?.(`Prefetch too large, skipping ${url}`);\n return;\n }\n this.prefetchedUrls.add(url);\n if (!this.onResourcesFetched) return;\n try {\n await this.onResourcesFetched(response, url);\n } catch (callbackError) {\n prefetchLogger.error?.(`Prefetch callback failed for ${url}`, callbackError);\n }\n } catch (error) {\n if (!isAbortError(error)) {\n prefetchLogger.error?.(`Failed to prefetch ${url}`, error);\n }\n } finally {\n if (timeoutId !== void 0) clearTimeout(timeoutId);\n this.controllers.delete(url);\n this.concurrent = Math.max(0, this.concurrent - 1);\n }\n }\n async prefetch(url) {\n const link = typeof document !== "undefined" ? document.createElement("a") : { href: url };\n link.href = url;\n await this.prefetchLink(link);\n }\n stopAll() {\n for (const controller of this.controllers.values()) {\n controller.abort();\n }\n this.controllers.clear();\n this.concurrent = 0;\n }\n isResponseTooLarge(response) {\n const rawLength = response.headers.get("content-length");\n if (rawLength === null) return false;\n const size = Number.parseInt(rawLength, 10);\n if (!Number.isFinite(size)) return false;\n return size > this.options.maxSize;\n }\n};\nvar prefetchQueue = new PrefetchQueue();\n\n// src/rendering/client/prefetch/resource-hints.ts\nvar ResourceHintsManager = class {\n constructor() {\n __publicField(this, "appliedHints", /* @__PURE__ */ new Set());\n }\n applyResourceHints(hints) {\n for (const hint of hints) {\n const key = `${hint.type}:${hint.href}`;\n if (this.appliedHints.has(key)) continue;\n const existing = document.querySelector(\n `link[rel="${hint.type}"][href="${hint.href}"]`\n );\n if (existing) {\n this.appliedHints.add(key);\n continue;\n }\n this.createAndAppendHint(hint);\n this.appliedHints.add(key);\n prefetchLogger.debug(`Added resource hint: ${hint.type} ${hint.href}`);\n }\n }\n createAndAppendHint(hint) {\n if (!document.head) {\n prefetchLogger.warn("document.head is not available, skipping resource hint");\n return;\n }\n const link = document.createElement("link");\n link.rel = hint.type;\n link.href = hint.href;\n if (hint.as) link.setAttribute("as", hint.as);\n if (hint.crossOrigin) link.setAttribute("crossorigin", hint.crossOrigin);\n if (hint.media) link.setAttribute("media", hint.media);\n document.head.appendChild(link);\n }\n extractResourceHints(html, prefetchedUrls) {\n try {\n const doc = new DOMParser().parseFromString(html, "text/html");\n const hints = [];\n this.extractPreloadLinks(doc, prefetchedUrls, hints);\n this.extractScripts(doc, prefetchedUrls, hints);\n this.extractStylesheets(doc, prefetchedUrls, hints);\n return hints;\n } catch (error) {\n prefetchLogger.error("Failed to parse prefetched page", error);\n return [];\n }\n }\n isValidResourceHintType(rel) {\n switch (rel) {\n case "prefetch":\n case "preload":\n case "preconnect":\n case "dns-prefetch":\n return true;\n default:\n return false;\n }\n }\n extractPreloadLinks(doc, prefetchedUrls, hints) {\n const links = doc.querySelectorAll(\n \'link[rel="preload"], link[rel="prefetch"]\'\n );\n for (const link of links) {\n const href = link.href;\n if (!href) continue;\n if (prefetchedUrls.has(href)) continue;\n if (!this.isValidResourceHintType(link.rel)) continue;\n hints.push({\n type: link.rel,\n href,\n as: link.getAttribute("as") ?? void 0\n });\n }\n }\n extractScripts(doc, prefetchedUrls, hints) {\n for (const script of doc.querySelectorAll("script[src]")) {\n const src = script.src;\n if (!src || prefetchedUrls.has(src)) continue;\n hints.push({ type: "prefetch", href: src, as: "script" });\n }\n }\n extractStylesheets(doc, prefetchedUrls, hints) {\n for (const link of doc.querySelectorAll(\'link[rel="stylesheet"]\')) {\n const href = link.href;\n if (!href || prefetchedUrls.has(href)) continue;\n hints.push({ type: "prefetch", href, as: "style" });\n }\n }\n static generateResourceHints(_route, assets) {\n const hints = [\n \'\',\n \'\',\n \'\'\n ];\n for (const asset of assets) {\n if (asset.endsWith(".js")) {\n hints.push(``);\n continue;\n }\n if (asset.endsWith(".css")) {\n hints.push(``);\n continue;\n }\n if (/\\.(woff2?|ttf|otf)$/.test(asset)) {\n hints.push(``);\n }\n }\n return hints.join("\\n");\n }\n};\n\n// src/rendering/client/browser-stubs/logger.ts\nfunction noop() {\n}\nvar logger = {\n debug: noop,\n info: console.log.bind(console),\n warn: console.warn.bind(console),\n error: console.error.bind(console),\n component: () => logger\n};\nvar PREFETCH_MAX_SIZE_BYTES2 = 200 * 1024;\nvar PREFETCH_DEFAULT_TIMEOUT_MS2 = 1e4;\nvar PREFETCH_DEFAULT_DELAY_MS2 = 200;\n\n// src/rendering/client/prefetch.ts\nvar PrefetchManager = class {\n constructor(options = {}) {\n __publicField(this, "options");\n __publicField(this, "prefetchedUrls", /* @__PURE__ */ new Set());\n __publicField(this, "networkUtils");\n __publicField(this, "linkObserver", null);\n __publicField(this, "resourceHintsManager");\n __publicField(this, "prefetchQueue");\n this.options = {\n rootMargin: options.rootMargin ?? "50px",\n delay: options.delay ?? PREFETCH_DEFAULT_DELAY_MS2,\n maxConcurrent: options.maxConcurrent ?? 2,\n allowedNetworks: options.allowedNetworks ?? ["4g", "wifi", "ethernet"],\n maxSize: options.maxSize ?? PREFETCH_MAX_SIZE_BYTES2,\n timeout: options.timeout ?? PREFETCH_DEFAULT_TIMEOUT_MS2\n };\n this.networkUtils = new NetworkUtils(this.options.allowedNetworks);\n this.resourceHintsManager = new ResourceHintsManager();\n this.prefetchQueue = new PrefetchQueue(\n {\n maxConcurrent: this.options.maxConcurrent,\n maxSize: this.options.maxSize,\n timeout: this.options.timeout\n },\n this.prefetchedUrls\n );\n this.prefetchQueue.setResourceCallback(\n (response, url) => this.prefetchPageResources(response, url)\n );\n }\n init() {\n prefetchLogger.info("Initializing prefetch manager");\n if (!this.networkUtils.shouldPrefetch()) {\n prefetchLogger.info("Prefetching disabled due to network conditions");\n return;\n }\n this.linkObserver = new LinkObserver(\n {\n rootMargin: this.options.rootMargin,\n delay: this.options.delay,\n onLinkVisible: (link) => this.prefetchQueue.prefetchLink(link)\n },\n this.prefetchedUrls\n );\n this.linkObserver.init();\n this.networkUtils.onNetworkChange(() => {\n if (!this.networkUtils.shouldPrefetch()) this.prefetchQueue.stopAll();\n });\n }\n async prefetchPageResources(response, _pageUrl) {\n const html = await response.text();\n const hints = this.resourceHintsManager.extractResourceHints(html, this.prefetchedUrls);\n this.resourceHintsManager.applyResourceHints(hints);\n }\n applyResourceHints(hints) {\n this.resourceHintsManager.applyResourceHints(hints);\n }\n async prefetch(url) {\n await this.prefetchQueue.prefetch(url);\n }\n static generateResourceHints(route, assets) {\n return ResourceHintsManager.generateResourceHints(route, assets);\n }\n destroy() {\n this.linkObserver?.destroy();\n this.prefetchQueue.stopAll();\n this.prefetchedUrls.clear();\n }\n};\nfunction initPrefetch(options) {\n const prefetchManager = new PrefetchManager(options);\n if (document.readyState === "loading") {\n document.addEventListener("DOMContentLoaded", () => prefetchManager.init(), { once: true });\n } else {\n prefetchManager.init();\n }\n globalThis.veryFrontPrefetch = prefetchManager;\n return prefetchManager;\n}\nfunction resolveAutoInitOptions() {\n const setting = globalThis.__VERYFRONT_PREFETCH__;\n if (!setting) return null;\n if (setting === true) return {};\n if (typeof setting === "object") return setting;\n return null;\n}\nfunction shouldAutoInitPrefetch(options) {\n if (!options) return false;\n if (typeof window === "undefined" || typeof document === "undefined") return false;\n const win = window;\n const doc = document;\n if (win.__veryfrontSSRStub || doc.__veryfrontSSRStub) return false;\n if (typeof IntersectionObserver === "undefined") return false;\n if (typeof MutationObserver === "undefined") return false;\n return true;\n}\nvar autoInitOptions = resolveAutoInitOptions();\nif (shouldAutoInitPrefetch(autoInitOptions)) initPrefetch(autoInitOptions);\nexport {\n PrefetchManager,\n initPrefetch\n};\n'; + 'var __defProp = Object.defineProperty;\nvar __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;\nvar __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);\n\n// src/rendering/client/browser-logger.ts\nvar ConditionalBrowserLogger = class {\n constructor(prefix, level) {\n __publicField(this, "prefix", prefix);\n __publicField(this, "level", level);\n }\n log(minLevel, fn, message, ...args) {\n if (this.level > minLevel) return;\n fn?.(message, ...args);\n }\n debug(message, ...args) {\n this.log(\n 0 /* DEBUG */,\n console.debug,\n `[${this.prefix}] DEBUG: ${message}`,\n ...args\n );\n }\n info(message, ...args) {\n this.log(1 /* INFO */, console.log, `[${this.prefix}] ${message}`, ...args);\n }\n warn(message, ...args) {\n this.log(\n 2 /* WARN */,\n console.warn,\n `[${this.prefix}] WARN: ${message}`,\n ...args\n );\n }\n error(message, ...args) {\n this.log(\n 3 /* ERROR */,\n console.error,\n `[${this.prefix}] ERROR: ${message}`,\n ...args\n );\n }\n};\nfunction getBrowserLogLevel() {\n if (typeof window === "undefined") return 2 /* WARN */;\n const g = globalThis;\n const isDevelopment = g.__VERYFRONT_DEV__ || g.__RSC_DEV__;\n if (!isDevelopment) return 2 /* WARN */;\n const isDebugEnabled2 = g.__VERYFRONT_DEBUG__ || g.__RSC_DEBUG__;\n return isDebugEnabled2 ? 0 /* DEBUG */ : 1 /* INFO */;\n}\nvar defaultLevel = getBrowserLogLevel();\nvar rscLogger = new ConditionalBrowserLogger("RSC", defaultLevel);\nvar prefetchLogger = new ConditionalBrowserLogger("PREFETCH", defaultLevel);\nvar hydrateLogger = new ConditionalBrowserLogger("HYDRATE", defaultLevel);\nvar browserLogger = new ConditionalBrowserLogger("VERYFRONT", defaultLevel);\n\n// src/rendering/client/prefetch/link-observer.ts\nfunction isAnchorElement(element) {\n return typeof HTMLAnchorElement !== "undefined" ? element instanceof HTMLAnchorElement : element.tagName === "A";\n}\nvar LinkObserver = class {\n constructor(options, prefetchedUrls) {\n __publicField(this, "options");\n __publicField(this, "intersectionObserver", null);\n __publicField(this, "mutationObserver", null);\n __publicField(this, "prefetchedUrls");\n __publicField(this, "pendingTimeouts", /* @__PURE__ */ new Map());\n __publicField(this, "elementTimeoutMap", /* @__PURE__ */ new WeakMap());\n __publicField(this, "timeoutCounter", 0);\n this.options = options;\n this.prefetchedUrls = prefetchedUrls;\n }\n init() {\n this.createIntersectionObserver();\n this.observeLinks();\n this.setupMutationObserver();\n }\n createIntersectionObserver() {\n this.intersectionObserver = new IntersectionObserver(\n (entries) => this.handleIntersection(entries),\n { rootMargin: this.options.rootMargin }\n );\n }\n handleIntersection(entries) {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n if (!isAnchorElement(entry.target)) continue;\n const link = entry.target;\n if (this.timeoutCounter > 1e6) this.timeoutCounter = 0;\n const timeoutKey = this.timeoutCounter++;\n const timeoutId = setTimeout(() => {\n this.pendingTimeouts.delete(timeoutKey);\n this.elementTimeoutMap.delete(link);\n this.options.onLinkVisible(link);\n }, this.options.delay);\n this.pendingTimeouts.set(timeoutKey, timeoutId);\n this.elementTimeoutMap.set(link, timeoutKey);\n }\n }\n observeLinks() {\n this.observeAnchors(document.querySelectorAll(\'a[href^="/"], a[href^="./"]\'));\n }\n setupMutationObserver() {\n this.mutationObserver = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n if (mutation.type !== "childList") continue;\n for (const node of mutation.addedNodes) {\n if (node.nodeType !== Node.ELEMENT_NODE) continue;\n this.observeElement(node);\n }\n for (const node of mutation.removedNodes) {\n if (node.nodeType !== Node.ELEMENT_NODE) continue;\n this.clearElementTimeouts(node);\n }\n }\n });\n this.mutationObserver.observe(document.body, { childList: true, subtree: true });\n }\n clearTimeoutForElement(element) {\n const timeoutKey = this.elementTimeoutMap.get(element);\n if (timeoutKey === void 0) return;\n const timeoutId = this.pendingTimeouts.get(timeoutKey);\n if (timeoutId !== void 0) {\n clearTimeout(timeoutId);\n this.pendingTimeouts.delete(timeoutKey);\n }\n this.elementTimeoutMap.delete(element);\n }\n clearElementTimeouts(element) {\n if (isAnchorElement(element)) this.clearTimeoutForElement(element);\n for (const link of element.querySelectorAll("a")) {\n this.clearTimeoutForElement(link);\n }\n }\n observeElement(element) {\n if (isAnchorElement(element) && this.isValidLink(element)) {\n this.intersectionObserver?.observe(element);\n }\n this.observeAnchors(element.querySelectorAll(\'a[href^="/"], a[href^="./"]\'));\n }\n observeAnchors(links) {\n for (const link of links) {\n if (!isAnchorElement(link)) continue;\n if (!this.isValidLink(link)) continue;\n this.intersectionObserver?.observe(link);\n }\n }\n isValidLink(link) {\n if (link.hostname !== globalThis.location.hostname) return false;\n if (link.hasAttribute("download")) return false;\n if (link.target === "_blank") return false;\n const url = link.href;\n if (this.prefetchedUrls.has(url)) return false;\n if (url === globalThis.location.href) return false;\n if (link.hash && link.pathname === globalThis.location.pathname) return false;\n if (link.dataset.noPrefetch) return false;\n return true;\n }\n destroy() {\n for (const timeoutId of this.pendingTimeouts.values()) {\n clearTimeout(timeoutId);\n }\n this.pendingTimeouts.clear();\n this.timeoutCounter = 0;\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = null;\n this.mutationObserver?.disconnect();\n this.mutationObserver = null;\n }\n};\n\n// src/rendering/client/prefetch/network-utils.ts\nvar NetworkUtils = class {\n constructor(allowedNetworks = ["4g", "wifi", "ethernet"]) {\n __publicField(this, "networkInfo");\n __publicField(this, "allowedNetworks");\n this.allowedNetworks = allowedNetworks;\n this.networkInfo = this.getNetworkConnection();\n }\n getNavigatorWithConnection() {\n if (typeof globalThis.navigator === "undefined") return null;\n return globalThis.navigator;\n }\n getNetworkConnection() {\n const nav = this.getNavigatorWithConnection();\n return nav?.connection ?? nav?.mozConnection ?? nav?.webkitConnection ?? null;\n }\n shouldPrefetch() {\n if (this.networkInfo?.saveData) return false;\n const effectiveType = this.networkInfo?.effectiveType;\n if (effectiveType != null && !this.allowedNetworks.includes(effectiveType)) return false;\n return true;\n }\n onNetworkChange(callback) {\n this.networkInfo?.addEventListener?.("change", callback);\n }\n getNetworkInfo() {\n return this.networkInfo;\n }\n};\n\n// src/utils/constants/css.ts\nvar MAX_CSS_FILE_BYTES = 16 * 1024 * 1024;\nvar MAX_CSS_TOTAL_BYTES = 64 * 1024 * 1024;\nvar MAX_CSS_OUTPUT_FILE_BYTES = 32 * 1024 * 1024;\n\n// src/utils/constants/buffers.ts\nvar DEFAULT_MAX_BODY_SIZE_BYTES = 1024 * 1024;\nvar DEFAULT_MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;\nvar PREFETCH_QUEUE_MAX_SIZE_BYTES = DEFAULT_MAX_BODY_SIZE_BYTES;\nvar MAX_BUNDLE_CHUNK_SIZE_BYTES = 4096 * 1024;\n\n// src/utils/constants/limits.ts\nvar MAX_TIMER_DELAY_MS = 2147483647;\n\n// src/utils/constants/cache.ts\nvar SECONDS_PER_MINUTE = 60;\nvar MINUTES_PER_HOUR = 60;\nvar HOURS_PER_DAY = 24;\nvar MS_PER_SECOND = 1e3;\nvar MS_PER_MINUTE = SECONDS_PER_MINUTE * MS_PER_SECOND;\nvar MS_PER_HOUR = MINUTES_PER_HOUR * MS_PER_MINUTE;\nvar ONE_DAY_MS = HOURS_PER_DAY * MS_PER_HOUR;\nfunction getEnvString(key) {\n const g = globalThis;\n try {\n return g.Deno?.env?.get?.(key) ?? g.process?.env?.[key];\n } catch (_) {\n return void 0;\n }\n}\nvar MAX_CONFIGURED_CACHE_ENTRIES = 1e6;\nvar MAX_CONFIGURED_CACHE_SIZE_MB = 64 * 1024;\nvar MAX_CONFIGURED_CONCURRENCY = 1e4;\nvar MAX_CONFIGURED_TTL_SECONDS = 365 * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE;\nvar BYTES_PER_MB = 1024 * 1024;\nvar MAX_CACHE_TTL_SECONDS = 2147483647;\nvar MAX_CACHE_TTL_MILLISECONDS = MAX_CACHE_TTL_SECONDS * MS_PER_SECOND;\nfunction getEnvInteger(key, fallback, { min = 1, max }) {\n const value = getEnvString(key);\n if (value == null) return fallback;\n const normalized = value.trim();\n if (!/^\\d+$/.test(normalized)) return fallback;\n const parsed = Number(normalized);\n if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) return fallback;\n return parsed;\n}\nfunction getStrictEnvInteger(key, fallback, { min = 1, max }) {\n const value = getEnvString(key);\n if (value == null) return fallback;\n const normalized = value.trim();\n if (!/^\\d+$/.test(normalized)) {\n throw new RangeError(\n `${key} must be a base-10 integer between ${min} and ${max}`\n );\n }\n const parsed = Number(normalized);\n if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {\n throw new RangeError(`${key} must be between ${min} and ${max}`);\n }\n return parsed;\n}\nfunction getEnvCacheEntries(key, fallback) {\n return getEnvInteger(key, fallback, { max: MAX_CONFIGURED_CACHE_ENTRIES });\n}\nfunction getEnvCacheSizeMb(key, fallback) {\n return getEnvInteger(key, fallback, { max: MAX_CONFIGURED_CACHE_SIZE_MB });\n}\nfunction getEnvTtlSeconds(key, fallback) {\n return getEnvInteger(key, fallback, { max: MAX_CONFIGURED_TTL_SECONDS });\n}\nvar DEFAULT_LRU_MAX_ENTRIES = getEnvCacheEntries("LRU_DEFAULT_MAX_ENTRIES", 100);\nvar COMPONENT_LOADER_MAX_ENTRIES = getEnvCacheEntries("COMPONENT_LOADER_MAX_ENTRIES", 200);\nvar COMPONENT_LOADER_TTL_MS = 10 * MS_PER_MINUTE;\nvar MDX_RENDERER_MAX_ENTRIES = getEnvCacheEntries("MDX_RENDERER_MAX_ENTRIES", 500);\nvar MDX_RENDERER_TTL_MS = 10 * MS_PER_MINUTE;\nvar RENDERER_CORE_MAX_ENTRIES = getEnvCacheEntries("RENDERER_CORE_MAX_ENTRIES", 200);\nvar RENDERER_CORE_TTL_MS = 5 * MS_PER_MINUTE;\nvar TSX_LAYOUT_MAX_ENTRIES = getEnvCacheEntries("TSX_LAYOUT_MAX_ENTRIES", 100);\nvar TSX_LAYOUT_TTL_MS = 10 * MS_PER_MINUTE;\nvar TSX_LAYOUT_PER_PROJECT_MAX_ENTRIES = getEnvCacheEntries(\n "TSX_LAYOUT_PER_PROJECT_MAX_ENTRIES",\n Math.ceil(TSX_LAYOUT_MAX_ENTRIES / 10)\n);\nvar DATA_FETCHING_MAX_ENTRIES = getStrictEnvInteger(\n "DATA_FETCHING_MAX_ENTRIES",\n 500,\n { max: MAX_CONFIGURED_CACHE_ENTRIES }\n);\nvar DATA_FETCHING_MAX_ENTRIES_PER_PROJECT = getStrictEnvInteger(\n "DATA_FETCHING_MAX_ENTRIES_PER_PROJECT",\n Math.max(1, Math.ceil(DATA_FETCHING_MAX_ENTRIES / 5)),\n { max: DATA_FETCHING_MAX_ENTRIES }\n);\nvar dataFetchingMaxSizeMb = getStrictEnvInteger(\n "DATA_FETCHING_MAX_SIZE_MB",\n 50,\n { max: MAX_CONFIGURED_CACHE_SIZE_MB }\n);\nvar DATA_FETCHING_MAX_SIZE_BYTES = dataFetchingMaxSizeMb * BYTES_PER_MB;\nvar DATA_FETCHING_MAX_SIZE_BYTES_PER_PROJECT = getStrictEnvInteger(\n "DATA_FETCHING_MAX_SIZE_MB_PER_PROJECT",\n Math.max(1, Math.ceil(dataFetchingMaxSizeMb / 5)),\n { max: dataFetchingMaxSizeMb }\n) * BYTES_PER_MB;\nvar DATA_FETCHING_TTL_MS = 10 * MS_PER_MINUTE;\nvar DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS = getStrictEnvInteger(\n "DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS",\n 512,\n { max: MAX_CONFIGURED_CONCURRENCY }\n);\nvar DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS_PER_PROJECT = getStrictEnvInteger(\n "DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS_PER_PROJECT",\n Math.min(128, DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS),\n { max: DATA_FETCHING_MAX_CONCURRENT_EXECUTIONS }\n);\nvar MDX_CACHE_TTL_DEVELOPMENT_MS = 5 * MS_PER_MINUTE;\nvar BUNDLE_CACHE_TTL_DEVELOPMENT_MS = 5 * MS_PER_MINUTE;\nvar BUNDLE_MANIFEST_PROD_TTL_MS = 7 * ONE_DAY_MS;\nvar SERVER_ACTION_DEFAULT_TTL_SEC = MINUTES_PER_HOUR * SECONDS_PER_MINUTE;\nvar DISTRIBUTED_SSR_MODULE_TTL_PRODUCTION_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_SSR_MODULE_TTL_SEC",\n 6 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_SSR_MODULE_TTL_PREVIEW_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_SSR_MODULE_TTL_PREVIEW_SEC",\n 10 * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_TRANSFORM_TTL_PRODUCTION_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_TRANSFORM_TTL_SEC",\n 6 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_TRANSFORM_TTL_PREVIEW_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_TRANSFORM_TTL_PREVIEW_SEC",\n 10 * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_FILE_TTL_PRODUCTION_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_FILE_TTL_SEC",\n MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_FILE_TTL_PREVIEW_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_FILE_TTL_PREVIEW_SEC",\n 5 * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_CSS_TTL_PRODUCTION_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_CSS_TTL_SEC",\n 6 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n);\nvar DISTRIBUTED_CSS_TTL_PREVIEW_SEC = getEnvTtlSeconds(\n "DISTRIBUTED_CSS_TTL_PREVIEW_SEC",\n 10 * SECONDS_PER_MINUTE\n);\nvar LRU_DEFAULT_MAX_ENTRIES_V2 = getEnvCacheEntries("LRU_MAX_ENTRIES", 2e3);\nvar LRU_DEFAULT_MAX_SIZE_BYTES = getEnvCacheSizeMb("LRU_MAX_SIZE_MB", 200) * BYTES_PER_MB;\nvar MEMORY_CACHE_MAX_ENTRIES = getEnvCacheEntries("MEMORY_CACHE_MAX_ENTRIES", 2e3);\nvar MEMORY_CACHE_MAX_SIZE_BYTES = getEnvCacheSizeMb("MEMORY_CACHE_MAX_SIZE_MB", 50) * BYTES_PER_MB;\nvar FILE_CACHE_MAX_ENTRIES = getEnvCacheEntries("FILE_CACHE_MAX_ENTRIES", 1e3);\nvar FILE_CACHE_MAX_SIZE_MB = getEnvCacheSizeMb("FILE_CACHE_MAX_SIZE_MB", 100);\nvar MAX_CONCURRENT_REVALIDATIONS = getEnvInteger("MAX_CONCURRENT_REVALIDATIONS", 32, {\n max: MAX_CONFIGURED_CONCURRENCY\n});\nvar MAX_CONCURRENT_HTTP_FETCHES = getEnvInteger("MAX_CONCURRENT_HTTP_FETCHES", 50, {\n max: MAX_CONFIGURED_CONCURRENCY\n});\nvar REVALIDATION_TIMEOUT_MS = getEnvInteger("REVALIDATION_TIMEOUT_MS", 15e3, {\n max: MAX_TIMER_DELAY_MS\n});\nvar REVALIDATION_PER_PROJECT_LIMIT = getEnvInteger(\n "REVALIDATION_PER_PROJECT_LIMIT",\n Math.ceil(MAX_CONCURRENT_REVALIDATIONS / 3),\n { min: 0, max: MAX_CONFIGURED_CONCURRENCY }\n);\nvar BUNDLE_MANIFEST_DISTRIBUTED_TTL_SEC = getEnvTtlSeconds(\n "BUNDLE_MANIFEST_DISTRIBUTED_TTL_SEC",\n HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n // 24 hours (86400)\n);\nvar BUNDLE_MANIFEST_LRU_MAX_ENTRIES = getEnvCacheEntries(\n "BUNDLE_MANIFEST_LRU_MAX_ENTRIES",\n 5e3\n);\nvar BUNDLE_MANIFEST_MEMORY_MAX_METADATA_SIZE_BYTES = getEnvCacheSizeMb(\n "BUNDLE_MANIFEST_MEMORY_MAX_METADATA_SIZE_MB",\n 128\n) * BYTES_PER_MB;\nvar BUNDLE_MANIFEST_MEMORY_MAX_CODE_SIZE_BYTES = getEnvCacheSizeMb(\n "BUNDLE_MANIFEST_MEMORY_MAX_CODE_SIZE_MB",\n 256\n) * BYTES_PER_MB;\nvar HTTP_MODULE_CACHE_MAX_ENTRIES = getEnvCacheEntries(\n "HTTP_MODULE_CACHE_MAX_ENTRIES",\n 2e3\n);\nvar HTTP_MODULE_DISTRIBUTED_TTL_SEC = getEnvTtlSeconds(\n "HTTP_MODULE_DISTRIBUTED_TTL_SEC",\n HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n // 24 hours (86400)\n);\nvar TRANSFORM_DISTRIBUTED_TTL_SEC = getEnvTtlSeconds(\n "TRANSFORM_DISTRIBUTED_TTL_SEC",\n 6 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE\n // 6 hours (21600)\n);\nvar MODULE_CACHE_MAX_ENTRIES = getEnvCacheEntries("MODULE_CACHE_MAX_ENTRIES", 1e4);\nvar MODULE_CACHE_TTL_MS = getEnvInteger(\n "MODULE_CACHE_TTL_MS",\n 5 * MS_PER_MINUTE,\n // 5 minutes - short enough to pick up changes, long enough to cache\n { max: MAX_TIMER_DELAY_MS }\n);\nvar ESM_CACHE_MAX_ENTRIES = getEnvCacheEntries("ESM_CACHE_MAX_ENTRIES", 5e3);\nvar ESM_CACHE_TTL_MS = getEnvInteger(\n "ESM_CACHE_TTL_MS",\n 10 * MS_PER_MINUTE,\n // 10 minutes - external modules change less frequently\n { max: MAX_TIMER_DELAY_MS }\n);\n\n// src/platform/compat/primordials/array.ts\nvar ArrayPrototypeAt = Array.prototype.at;\nvar ArrayPrototypeFilter = Array.prototype.filter;\nvar ArrayPrototypeJoin = Array.prototype.join;\nvar ArrayPrototypeMap = Array.prototype.map;\nvar ArrayPrototypePop = Array.prototype.pop;\nvar ArrayPrototypePush = Array.prototype.push;\nvar ArrayPrototypeSort = Array.prototype.sort;\n\n// src/utils/constants/http.ts\nvar KB_IN_BYTES = 1024;\nvar PREFETCH_MAX_SIZE_BYTES = 200 * KB_IN_BYTES;\n\n// src/utils/constants/hmr.ts\nvar HMR_MAX_MESSAGE_SIZE_BYTES = 1024 * KB_IN_BYTES;\n\n// src/utils/constants/network.ts\nvar BYTES_PER_KB = 1024;\nvar BYTES_PER_MB2 = BYTES_PER_KB * BYTES_PER_KB;\n\n// src/utils/constants/security.ts\nvar MAX_CSRF_TTL_SECONDS = Number.MAX_SAFE_INTEGER;\n\n// src/platform/compat/constants.ts\nvar DEFAULT_PORT = 3e3;\nvar LOCALHOST = Object.freeze(\n {\n IPV4: "127.0.0.1",\n IPV6: "::1",\n HOSTNAME: "localhost"\n }\n);\n\n// src/config/defaults.ts\nvar DEFAULT_TIMEOUT_MS = 5e3;\nvar SSR_TIMEOUT_MS = 1e4;\nvar SSR_MAX_BUFFERED_BYTES = 16 * 1024 * 1024;\nvar SANDBOX_TIMEOUT_MS = 5e3;\nvar DEFAULT_CACHE_MAX_SIZE = 100;\nvar DURATION_HISTOGRAM_BOUNDARIES_MS = Object.freeze(\n [\n 5,\n 10,\n 25,\n 50,\n 75,\n 100,\n 250,\n 500,\n 750,\n 1e3,\n 2500,\n 5e3,\n 7500,\n 1e4\n ]\n);\nvar SIZE_HISTOGRAM_BOUNDARIES_KB = Object.freeze(\n [\n 1,\n 5,\n 10,\n 25,\n 50,\n 100,\n 250,\n 500,\n 1e3,\n 2500,\n 5e3,\n 1e4\n ]\n);\nvar defaultConfig = Object.freeze(\n {\n server: Object.freeze({\n port: DEFAULT_PORT,\n hostname: "0.0.0.0"\n }),\n timeouts: Object.freeze({\n default: DEFAULT_TIMEOUT_MS,\n api: 3e4,\n ssr: SSR_TIMEOUT_MS,\n hmr: 3e4,\n sandbox: SANDBOX_TIMEOUT_MS\n }),\n cache: Object.freeze({\n jit: Object.freeze({\n maxSize: DEFAULT_CACHE_MAX_SIZE,\n tempDirPrefix: "vf-bundle-"\n })\n }),\n metrics: Object.freeze({\n ssrBoundaries: DURATION_HISTOGRAM_BOUNDARIES_MS\n })\n }\n);\n\n// src/utils/constants/server.ts\nvar INTERNAL_PREFIX = "/_veryfront";\nvar INTERNAL_PATH_PREFIXES = {\n /** React Server Components endpoints */\n RSC: `${INTERNAL_PREFIX}/rsc/`,\n /** File system access endpoints (base64 encoded paths) */\n FS: `${INTERNAL_PREFIX}/fs/`,\n /** Virtual module system */\n MODULES: `${INTERNAL_PREFIX}/modules/`,\n /** Generated page modules */\n PAGES: `${INTERNAL_PREFIX}/pages/`,\n /** Data JSON endpoints */\n DATA: `${INTERNAL_PREFIX}/data/`,\n /** Library modules and large vendor surfaces */\n LIB: `${INTERNAL_PREFIX}/lib/`,\n /** Chunk assets */\n CHUNKS: `${INTERNAL_PREFIX}/chunks/`,\n /** Client component modules */\n CLIENT: `${INTERNAL_PREFIX}/client/`\n};\nvar INTERNAL_ENDPOINTS = {\n // Development endpoints\n HMR_RUNTIME: `${INTERNAL_PREFIX}/hmr-runtime.js`,\n HMR: `${INTERNAL_PREFIX}/hmr.js`,\n ERROR_OVERLAY: `${INTERNAL_PREFIX}/error-overlay.js`,\n // Legacy endpoint retained for backward compatibility (no active handler).\n DEV_LOADER: `${INTERNAL_PREFIX}/dev-loader.js`,\n CLIENT_LOG: `${INTERNAL_PREFIX}/log`,\n // Production endpoints\n CLIENT_JS: `${INTERNAL_PREFIX}/client.js`,\n ROUTER_JS: `${INTERNAL_PREFIX}/router.js`,\n PREFETCH_JS: `${INTERNAL_PREFIX}/prefetch.js`,\n MANIFEST_JSON: `${INTERNAL_PREFIX}/manifest.json`,\n APP_JS: `${INTERNAL_PREFIX}/app.js`,\n // RSC endpoints\n RSC_CLIENT: `${INTERNAL_PREFIX}/rsc/client.js`,\n RSC_MANIFEST: `${INTERNAL_PREFIX}/rsc/manifest`,\n RSC_STREAM: `${INTERNAL_PREFIX}/rsc/stream`,\n RSC_PAYLOAD: `${INTERNAL_PREFIX}/rsc/payload`,\n RSC_RENDER: `${INTERNAL_PREFIX}/rsc/render`,\n RSC_PAGE: `${INTERNAL_PREFIX}/rsc/page`,\n RSC_MODULE: `${INTERNAL_PREFIX}/rsc/module`,\n RSC_DOM: `${INTERNAL_PREFIX}/rsc/dom.js`,\n // Library module endpoints\n LIB_CHAT_REACT: `${INTERNAL_PREFIX}/lib/chat/react.js`,\n LIB_CHAT_COMPONENTS: `${INTERNAL_PREFIX}/lib/chat/components.js`,\n LIB_CHAT_PRIMITIVES: `${INTERNAL_PREFIX}/lib/chat/primitives.js`\n};\nvar PROJECT_DIRS = {\n /** Base veryfront internal directory */\n ROOT: ".veryfront",\n /** Cache directory for build artifacts, transforms, etc. */\n CACHE: ".veryfront/cache",\n /** KV store directory */\n KV: ".veryfront/kv",\n /** Log files directory */\n LOGS: ".veryfront/logs",\n /** Temporary files directory */\n TMP: ".veryfront/tmp"\n};\nvar DEFAULT_CACHE_DIR = PROJECT_DIRS.CACHE;\nvar DEV_SERVER_ENDPOINTS = {\n HMR_RUNTIME: INTERNAL_ENDPOINTS.HMR_RUNTIME,\n ERROR_OVERLAY: INTERNAL_ENDPOINTS.ERROR_OVERLAY\n};\n\n// src/rendering/client/prefetch/prefetch-queue.ts\nvar DEFAULT_OPTIONS = {\n maxConcurrent: 4,\n maxSize: PREFETCH_QUEUE_MAX_SIZE_BYTES,\n timeout: 5e3\n};\nfunction isAbortError(error) {\n if (typeof error !== "object" || error === null) return false;\n if (!("name" in error)) return false;\n return error.name === "AbortError";\n}\nvar PrefetchQueue = class {\n constructor(options = {}, prefetchedUrls) {\n __publicField(this, "options");\n __publicField(this, "controllers", /* @__PURE__ */ new Map());\n __publicField(this, "prefetchedUrls");\n __publicField(this, "concurrent", 0);\n __publicField(this, "stopped", false);\n __publicField(this, "onResourcesFetched");\n this.options = { ...DEFAULT_OPTIONS, ...options };\n this.prefetchedUrls = prefetchedUrls ?? /* @__PURE__ */ new Set();\n }\n setResourceCallback(callback) {\n this.onResourcesFetched = callback;\n }\n enqueue(url) {\n void this.prefetch(url);\n }\n has(url) {\n return this.prefetchedUrls.has(url) || this.controllers.has(url);\n }\n get size() {\n return this.controllers.size;\n }\n clear() {\n this.stopAll();\n this.prefetchedUrls.clear();\n }\n start() {\n this.stopped = false;\n }\n stop() {\n this.stopped = true;\n this.stopAll();\n }\n getQueueSize() {\n return this.controllers.size;\n }\n getConcurrentCount() {\n return this.concurrent;\n }\n async prefetchLink(link) {\n if (this.stopped) return;\n const url = link.href;\n if (!url || this.controllers.has(url) || this.prefetchedUrls.has(url)) return;\n if (this.concurrent >= this.options.maxConcurrent) {\n prefetchLogger.debug?.(`Prefetch queue full, skipping ${url}`);\n return;\n }\n let parsedUrl;\n try {\n parsedUrl = new URL(url);\n } catch (_) {\n prefetchLogger.debug?.(`Invalid prefetch URL ${url}`);\n return;\n }\n const controller = new AbortController();\n this.controllers.set(url, controller);\n this.concurrent += 1;\n const timeoutId = this.options.timeout > 0 ? setTimeout(() => controller.abort(), this.options.timeout) : void 0;\n try {\n const response = await fetch(parsedUrl.toString(), {\n method: "GET",\n signal: controller.signal,\n headers: { "X-Veryfront-Prefetch": "1" }\n });\n if (!response.ok) return;\n if (this.isResponseTooLarge(response)) {\n prefetchLogger.debug?.(`Prefetch too large, skipping ${url}`);\n return;\n }\n this.prefetchedUrls.add(url);\n if (!this.onResourcesFetched) return;\n try {\n await this.onResourcesFetched(response, url);\n } catch (callbackError) {\n prefetchLogger.error?.(`Prefetch callback failed for ${url}`, callbackError);\n }\n } catch (error) {\n if (!isAbortError(error)) {\n prefetchLogger.error?.(`Failed to prefetch ${url}`, error);\n }\n } finally {\n if (timeoutId !== void 0) clearTimeout(timeoutId);\n this.controllers.delete(url);\n this.concurrent = Math.max(0, this.concurrent - 1);\n }\n }\n async prefetch(url) {\n const link = typeof document !== "undefined" ? document.createElement("a") : { href: url };\n link.href = url;\n await this.prefetchLink(link);\n }\n stopAll() {\n for (const controller of this.controllers.values()) {\n controller.abort();\n }\n this.controllers.clear();\n this.concurrent = 0;\n }\n isResponseTooLarge(response) {\n const rawLength = response.headers.get("content-length");\n if (rawLength === null) return false;\n const size = Number.parseInt(rawLength, 10);\n if (!Number.isFinite(size)) return false;\n return size > this.options.maxSize;\n }\n};\nvar prefetchQueue = new PrefetchQueue();\n\n// src/rendering/client/prefetch/resource-hints.ts\nvar ResourceHintsManager = class {\n constructor() {\n __publicField(this, "appliedHints", /* @__PURE__ */ new Set());\n }\n applyResourceHints(hints) {\n for (const hint of hints) {\n const key = `${hint.type}:${hint.href}`;\n if (this.appliedHints.has(key)) continue;\n const existing = document.querySelector(\n `link[rel="${hint.type}"][href="${hint.href}"]`\n );\n if (existing) {\n this.appliedHints.add(key);\n continue;\n }\n this.createAndAppendHint(hint);\n this.appliedHints.add(key);\n prefetchLogger.debug(`Added resource hint: ${hint.type} ${hint.href}`);\n }\n }\n createAndAppendHint(hint) {\n if (!document.head) {\n prefetchLogger.warn("document.head is not available, skipping resource hint");\n return;\n }\n const link = document.createElement("link");\n link.rel = hint.type;\n link.href = hint.href;\n if (hint.as) link.setAttribute("as", hint.as);\n if (hint.crossOrigin) link.setAttribute("crossorigin", hint.crossOrigin);\n if (hint.media) link.setAttribute("media", hint.media);\n document.head.appendChild(link);\n }\n extractResourceHints(html, prefetchedUrls) {\n try {\n const doc = new DOMParser().parseFromString(html, "text/html");\n const hints = [];\n this.extractPreloadLinks(doc, prefetchedUrls, hints);\n this.extractScripts(doc, prefetchedUrls, hints);\n this.extractStylesheets(doc, prefetchedUrls, hints);\n return hints;\n } catch (error) {\n prefetchLogger.error("Failed to parse prefetched page", error);\n return [];\n }\n }\n isValidResourceHintType(rel) {\n switch (rel) {\n case "prefetch":\n case "preload":\n case "preconnect":\n case "dns-prefetch":\n return true;\n default:\n return false;\n }\n }\n extractPreloadLinks(doc, prefetchedUrls, hints) {\n const links = doc.querySelectorAll(\n \'link[rel="preload"], link[rel="prefetch"]\'\n );\n for (const link of links) {\n const href = link.href;\n if (!href) continue;\n if (prefetchedUrls.has(href)) continue;\n if (!this.isValidResourceHintType(link.rel)) continue;\n hints.push({\n type: link.rel,\n href,\n as: link.getAttribute("as") ?? void 0\n });\n }\n }\n extractScripts(doc, prefetchedUrls, hints) {\n for (const script of doc.querySelectorAll("script[src]")) {\n const src = script.src;\n if (!src || prefetchedUrls.has(src)) continue;\n hints.push({ type: "prefetch", href: src, as: "script" });\n }\n }\n extractStylesheets(doc, prefetchedUrls, hints) {\n for (const link of doc.querySelectorAll(\'link[rel="stylesheet"]\')) {\n const href = link.href;\n if (!href || prefetchedUrls.has(href)) continue;\n hints.push({ type: "prefetch", href, as: "style" });\n }\n }\n static generateResourceHints(_route, assets) {\n const hints = [\n \'\',\n \'\',\n \'\'\n ];\n for (const asset of assets) {\n if (asset.endsWith(".js")) {\n hints.push(``);\n continue;\n }\n if (asset.endsWith(".css")) {\n hints.push(``);\n continue;\n }\n if (/\\.(woff2?|ttf|otf)$/.test(asset)) {\n hints.push(``);\n }\n }\n return hints.join("\\n");\n }\n};\n\n// src/rendering/client/browser-stubs/logger.ts\nfunction noop() {\n}\nvar logger = {\n debug: noop,\n info: console.log.bind(console),\n warn: console.warn.bind(console),\n error: console.error.bind(console),\n component: () => logger\n};\nvar PREFETCH_MAX_SIZE_BYTES2 = 200 * 1024;\nvar PREFETCH_DEFAULT_TIMEOUT_MS2 = 1e4;\nvar PREFETCH_DEFAULT_DELAY_MS2 = 200;\n\n// src/rendering/client/prefetch.ts\nvar PrefetchManager = class {\n constructor(options = {}) {\n __publicField(this, "options");\n __publicField(this, "prefetchedUrls", /* @__PURE__ */ new Set());\n __publicField(this, "networkUtils");\n __publicField(this, "linkObserver", null);\n __publicField(this, "resourceHintsManager");\n __publicField(this, "prefetchQueue");\n this.options = {\n rootMargin: options.rootMargin ?? "50px",\n delay: options.delay ?? PREFETCH_DEFAULT_DELAY_MS2,\n maxConcurrent: options.maxConcurrent ?? 2,\n allowedNetworks: options.allowedNetworks ?? ["4g", "wifi", "ethernet"],\n maxSize: options.maxSize ?? PREFETCH_MAX_SIZE_BYTES2,\n timeout: options.timeout ?? PREFETCH_DEFAULT_TIMEOUT_MS2\n };\n this.networkUtils = new NetworkUtils(this.options.allowedNetworks);\n this.resourceHintsManager = new ResourceHintsManager();\n this.prefetchQueue = new PrefetchQueue(\n {\n maxConcurrent: this.options.maxConcurrent,\n maxSize: this.options.maxSize,\n timeout: this.options.timeout\n },\n this.prefetchedUrls\n );\n this.prefetchQueue.setResourceCallback(\n (response, url) => this.prefetchPageResources(response, url)\n );\n }\n init() {\n prefetchLogger.info("Initializing prefetch manager");\n if (!this.networkUtils.shouldPrefetch()) {\n prefetchLogger.info("Prefetching disabled due to network conditions");\n return;\n }\n this.linkObserver = new LinkObserver(\n {\n rootMargin: this.options.rootMargin,\n delay: this.options.delay,\n onLinkVisible: (link) => this.prefetchQueue.prefetchLink(link)\n },\n this.prefetchedUrls\n );\n this.linkObserver.init();\n this.networkUtils.onNetworkChange(() => {\n if (!this.networkUtils.shouldPrefetch()) this.prefetchQueue.stopAll();\n });\n }\n async prefetchPageResources(response, _pageUrl) {\n const html = await response.text();\n const hints = this.resourceHintsManager.extractResourceHints(html, this.prefetchedUrls);\n this.resourceHintsManager.applyResourceHints(hints);\n }\n applyResourceHints(hints) {\n this.resourceHintsManager.applyResourceHints(hints);\n }\n async prefetch(url) {\n await this.prefetchQueue.prefetch(url);\n }\n static generateResourceHints(route, assets) {\n return ResourceHintsManager.generateResourceHints(route, assets);\n }\n destroy() {\n this.linkObserver?.destroy();\n this.prefetchQueue.stopAll();\n this.prefetchedUrls.clear();\n }\n};\nfunction initPrefetch(options) {\n const prefetchManager = new PrefetchManager(options);\n if (document.readyState === "loading") {\n document.addEventListener("DOMContentLoaded", () => prefetchManager.init(), { once: true });\n } else {\n prefetchManager.init();\n }\n globalThis.veryFrontPrefetch = prefetchManager;\n return prefetchManager;\n}\nfunction resolveAutoInitOptions() {\n const setting = globalThis.__VERYFRONT_PREFETCH__;\n if (!setting) return null;\n if (setting === true) return {};\n if (typeof setting === "object") return setting;\n return null;\n}\nfunction shouldAutoInitPrefetch(options) {\n if (!options) return false;\n if (typeof window === "undefined" || typeof document === "undefined") return false;\n const win = window;\n const doc = document;\n if (win.__veryfrontSSRStub || doc.__veryfrontSSRStub) return false;\n if (typeof IntersectionObserver === "undefined") return false;\n if (typeof MutationObserver === "undefined") return false;\n return true;\n}\nvar autoInitOptions = resolveAutoInitOptions();\nif (shouldAutoInitPrefetch(autoInitOptions)) initPrefetch(autoInitOptions);\nexport {\n PrefetchManager,\n initPrefetch\n};\n'; diff --git a/src/cache/config-hash.test.ts b/src/cache/config-hash.test.ts index 6d54658930..75091026a2 100644 --- a/src/cache/config-hash.test.ts +++ b/src/cache/config-hash.test.ts @@ -1,6 +1,14 @@ import "#veryfront/schemas/_test-setup.ts"; import { assertEquals, assertNotEquals } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; +import { buildDependencyPinningCacheVariant } from "#veryfront/cache/keys/dependency-pinning.ts"; +import { + CSSTYPE_VERSION, + DEFAULT_REACT_VERSION, + TAILWIND_VERSION, +} from "#veryfront/transforms/import-rewriter/url-builder.ts"; +import { computeHash } from "#veryfront/utils/hash-utils.ts"; +import { VERSION } from "#veryfront/utils/version.ts"; import { computeConfigHash, computeConfigHashSync } from "./config-hash.ts"; const CANONICAL_PIN_KEY = "on:z7bg3qnfgtcb"; @@ -8,6 +16,84 @@ const CHANGED_CANONICAL_PIN_KEY = "on:z7bg3qnfgtcc"; describe("cache/config-hash", () => { describe("computeConfigHash", () => { + it("preserves the established serialized identity for the default config", async () => { + const identity = JSON.stringify({ + transformVersion: VERSION, + reactVersion: DEFAULT_REACT_VERSION, + jsxImportSource: "react", + moduleServerUrl: null, + vendorBundleHash: null, + apiBaseUrl: null, + studioEmbed: false, + dev: false, + csstype: CSSTYPE_VERSION, + tailwind: TAILWIND_VERSION, + }); + assertEquals( + await computeConfigHash({}), + await computeHash(identity), + ); + }); + + it("preserves the established serialized identity for a fully scoped config", async () => { + const dependencyPinningCacheVariant = buildDependencyPinningCacheVariant( + CANONICAL_PIN_KEY, + "https://preview.example.test", + ); + const identity = JSON.stringify({ + transformVersion: VERSION, + reactVersion: "18.3.1", + jsxImportSource: "preact", + moduleServerUrl: "https://modules.example.test/_vf_modules", + vendorBundleHash: "vendor-a", + apiBaseUrl: "https://api.example.test", + studioEmbed: true, + dev: true, + ...(dependencyPinningCacheVariant ? { dependencyPinningCacheVariant } : {}), + csstype: CSSTYPE_VERSION, + tailwind: TAILWIND_VERSION, + }); + assertEquals( + await computeConfigHash({ + reactVersion: "18.3.1", + jsxImportSource: "preact", + moduleServerUrl: "https://modules.example.test/_vf_modules", + moduleServerOrigin: "https://preview.example.test", + vendorBundleHash: "vendor-a", + apiBaseUrl: "https://api.example.test", + studioEmbed: true, + dev: true, + dependencyPinningCacheKey: CANONICAL_PIN_KEY, + }), + await computeHash(identity), + ); + }); + + it("keeps distinct hashes stable when array push and join are poisoned", async () => { + const firstConfig = { reactVersion: "18.3.1", dev: false }; + const secondConfig = { reactVersion: "19.2.4", dev: true }; + const firstBaseline = await computeConfigHash(firstConfig); + const secondBaseline = await computeConfigHash(secondConfig); + const originalPush = Array.prototype.push; + const originalJoin = Array.prototype.join; + let firstPoisoned: string | undefined; + let secondPoisoned: string | undefined; + + try { + Reflect.set(Array.prototype, "push", () => 0); + Reflect.set(Array.prototype, "join", () => "poisoned"); + firstPoisoned = await computeConfigHash(firstConfig); + secondPoisoned = await computeConfigHash(secondConfig); + } finally { + Reflect.set(Array.prototype, "push", originalPush); + Reflect.set(Array.prototype, "join", originalJoin); + } + + assertEquals(firstPoisoned, firstBaseline); + assertEquals(secondPoisoned, secondBaseline); + assertNotEquals(firstPoisoned, secondPoisoned); + }); + it("should return a 64-char hex hash", async () => { const hash = await computeConfigHash({}); assertEquals(hash.length, 64); @@ -115,6 +201,83 @@ describe("cache/config-hash", () => { }); describe("computeConfigHashSync", () => { + it("matches the golden identity for the default transform config", () => { + assertEquals( + computeConfigHashSync({}), + `v${VERSION}:${DEFAULT_REACT_VERSION}:react`, + ); + }); + + it("matches the golden identity for a fully scoped transform config", () => { + assertEquals( + computeConfigHashSync({ + reactVersion: "18.3.1", + jsxImportSource: "preact", + moduleServerUrl: "https://modules.example.test/_vf_modules", + moduleServerOrigin: "https://preview.example.test", + vendorBundleHash: "vendor-a", + apiBaseUrl: "https://api.example.test", + studioEmbed: true, + dev: true, + dependencyPinningCacheKey: CANONICAL_PIN_KEY, + }), + `v${VERSION}:18.3.1:preact:modules:40:https://modules.example.test/_vf_modules:vendor:8:vendor-a:api:24:https://api.example.test:studio:dev:pins:on:z7bg3qnfgtcb:origin:aHR0cHM6Ly9wcmV2aWV3LmV4YW1wbGUudGVzdA`, + ); + }); + + it("preserves the established identity after array primordial poisoning", () => { + const originalFilter = Array.prototype.filter; + const originalJoin = Array.prototype.join; + const originalPush = Array.prototype.push; + let identity: string | undefined; + try { + Reflect.set(Array.prototype, "filter", () => []); + Reflect.set(Array.prototype, "join", () => "poisoned"); + Reflect.set(Array.prototype, "push", () => 0); + identity = computeConfigHashSync({ + moduleServerUrl: "https://modules.example.test/_vf_modules", + vendorBundleHash: "vendor-a", + apiBaseUrl: "https://api.example.test", + studioEmbed: true, + dev: true, + }); + } finally { + Reflect.set(Array.prototype, "filter", originalFilter); + Reflect.set(Array.prototype, "join", originalJoin); + Reflect.set(Array.prototype, "push", originalPush); + } + + assertEquals( + identity, + `v${VERSION}:${DEFAULT_REACT_VERSION}:react:modules:40:https://modules.example.test/_vf_modules:vendor:8:vendor-a:api:24:https://api.example.test:studio:dev`, + ); + }); + + it("keeps distinct sync identities stable when array push and join are poisoned", () => { + const firstConfig = { reactVersion: "18.3.1", dev: false }; + const secondConfig = { reactVersion: "19.2.4", dev: true }; + const firstBaseline = computeConfigHashSync(firstConfig); + const secondBaseline = computeConfigHashSync(secondConfig); + const originalPush = Array.prototype.push; + const originalJoin = Array.prototype.join; + let firstPoisoned: string | undefined; + let secondPoisoned: string | undefined; + + try { + Reflect.set(Array.prototype, "push", () => 0); + Reflect.set(Array.prototype, "join", () => "poisoned"); + firstPoisoned = computeConfigHashSync(firstConfig); + secondPoisoned = computeConfigHashSync(secondConfig); + } finally { + Reflect.set(Array.prototype, "push", originalPush); + Reflect.set(Array.prototype, "join", originalJoin); + } + + assertEquals(firstPoisoned, firstBaseline); + assertEquals(secondPoisoned, secondBaseline); + assertNotEquals(firstPoisoned, secondPoisoned); + }); + it("should return a string", () => { const hash = computeConfigHashSync({}); assertEquals(typeof hash, "string"); diff --git a/src/cache/config-hash.ts b/src/cache/config-hash.ts index 87291deda1..7dfeb9102e 100644 --- a/src/cache/config-hash.ts +++ b/src/cache/config-hash.ts @@ -7,6 +7,10 @@ import { computeHash } from "#veryfront/utils"; import { VERSION } from "#veryfront/utils/version.ts"; +import { + primordialArrayJoin as arrayJoin, + primordialArrayPush as arrayPush, +} from "#veryfront/platform/compat/primordials/array.ts"; import { CSSTYPE_VERSION, DEFAULT_REACT_VERSION, @@ -14,6 +18,9 @@ import { } from "#veryfront/transforms/import-rewriter/url-builder.ts"; import { buildDependencyPinningCacheVariant } from "./keys/dependency-pinning.ts"; +const JSONStringify = JSON.stringify; +const ObjectCreate = Object.create; + /** * Configuration that affects transform output. */ @@ -48,21 +55,24 @@ export function computeConfigHash(config: TransformConfig): Promise { config.dependencyPinningCacheKey, config.moduleServerOrigin, ); - const normalized = { - transformVersion: VERSION, - reactVersion: config.reactVersion ?? DEFAULT_REACT_VERSION, - jsxImportSource: config.jsxImportSource ?? "react", - moduleServerUrl: config.moduleServerUrl ?? null, - vendorBundleHash: config.vendorBundleHash ?? null, - apiBaseUrl: config.apiBaseUrl ?? null, - studioEmbed: config.studioEmbed ?? false, - dev: config.dev ?? false, - ...(dependencyPinningCacheVariant ? { dependencyPinningCacheVariant } : {}), - csstype: CSSTYPE_VERSION, - tailwind: TAILWIND_VERSION, - }; + // Null-prototype storage preserves the existing JSON cache-key format while + // preventing project code from injecting an inherited toJSON hook. + const normalized = ObjectCreate(null) as Record; + normalized.transformVersion = VERSION; + normalized.reactVersion = config.reactVersion ?? DEFAULT_REACT_VERSION; + normalized.jsxImportSource = config.jsxImportSource ?? "react"; + normalized.moduleServerUrl = config.moduleServerUrl ?? null; + normalized.vendorBundleHash = config.vendorBundleHash ?? null; + normalized.apiBaseUrl = config.apiBaseUrl ?? null; + normalized.studioEmbed = config.studioEmbed ?? false; + normalized.dev = config.dev ?? false; + if (dependencyPinningCacheVariant) { + normalized.dependencyPinningCacheVariant = dependencyPinningCacheVariant; + } + normalized.csstype = CSSTYPE_VERSION; + normalized.tailwind = TAILWIND_VERSION; - return computeHash(JSON.stringify(normalized)); + return computeHash(JSONStringify(normalized)); } /** @@ -71,25 +81,27 @@ export function computeConfigHash(config: TransformConfig): Promise { * Use this when you need a config hash but can't afford async overhead. */ export function computeConfigHashSync(config: TransformConfig): string { - const parts = [ - `v${VERSION}`, - config.reactVersion ?? DEFAULT_REACT_VERSION, - config.jsxImportSource ?? "react", - encodeConfigPart("modules", config.moduleServerUrl), - encodeConfigPart("vendor", config.vendorBundleHash), - encodeConfigPart("api", config.apiBaseUrl), - config.studioEmbed ? "studio" : "", - config.dev ? "dev" : "", - ].filter(Boolean); + const parts: string[] = []; + arrayPush(parts, `v${VERSION}`); + arrayPush(parts, config.reactVersion ?? DEFAULT_REACT_VERSION); + arrayPush(parts, config.jsxImportSource ?? "react"); + const moduleServerUrlPart = encodeConfigPart("modules", config.moduleServerUrl); + if (moduleServerUrlPart) arrayPush(parts, moduleServerUrlPart); + const vendorBundleHashPart = encodeConfigPart("vendor", config.vendorBundleHash); + if (vendorBundleHashPart) arrayPush(parts, vendorBundleHashPart); + const apiBaseUrlPart = encodeConfigPart("api", config.apiBaseUrl); + if (apiBaseUrlPart) arrayPush(parts, apiBaseUrlPart); + if (config.studioEmbed) arrayPush(parts, "studio"); + if (config.dev) arrayPush(parts, "dev"); const dependencyPinningCacheVariant = buildDependencyPinningCacheVariant( config.dependencyPinningCacheKey, config.moduleServerOrigin, ); if (dependencyPinningCacheVariant) { - parts.push(`pins:${dependencyPinningCacheVariant}`); + arrayPush(parts, `pins:${dependencyPinningCacheVariant}`); } - return parts.join(":"); + return arrayJoin(parts, ":"); } function encodeConfigPart(label: string, value: string | undefined): string { diff --git a/src/embedding/rag-store.test.ts b/src/embedding/rag-store.test.ts index 772ed1b5cf..bf7873fc22 100644 --- a/src/embedding/rag-store.test.ts +++ b/src/embedding/rag-store.test.ts @@ -839,14 +839,36 @@ describe("ragStore", () => { const searchPromise = store.search("needle"); await queryEmbeddingStarted; + const listDocumentsPromise = store.listDocuments(); + let blockedTimer: ReturnType | undefined; + let observedDocuments: Awaited> | "blocked"; + try { + observedDocuments = await Promise.race([ + listDocumentsPromise, + new Promise<"blocked">((resolve) => { + blockedTimer = setTimeout(() => resolve("blocked"), 5_000); + }), + ]); + } finally { + if (blockedTimer !== undefined) clearTimeout(blockedTimer); + releaseQueryEmbedding(); + } + let settleTimer: ReturnType | undefined; const documents = await Promise.race([ - store.listDocuments(), - new Promise<"blocked">((resolve) => setTimeout(() => resolve("blocked"), 50)), - ]); - releaseQueryEmbedding(); + listDocumentsPromise, + new Promise((_, reject) => { + settleTimer = setTimeout( + () => reject(new Error("listDocuments did not settle after query embedding release")), + 5_000, + ); + }), + ]).finally(() => { + if (settleTimer !== undefined) clearTimeout(settleTimer); + }); await searchPromise; - assert(Array.isArray(documents)); + assert(Array.isArray(observedDocuments)); + assertEquals(observedDocuments, documents); assertEquals(documents.length, 1); assertEquals(documents[0]?.title, "Doc"); }); diff --git a/src/modules/README.md b/src/modules/README.md index c6c7b63461..14506c243f 100644 --- a/src/modules/README.md +++ b/src/modules/README.md @@ -142,12 +142,14 @@ module before importing it. ```typescript import { loadImportMap, mergeImportMaps, resolveImport } from "#veryfront/modules"; +import type { VeryfrontConfig } from "#veryfront/config"; import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; export async function resolveReact( adapter: RuntimeAdapter, + validatedConfig?: VeryfrontConfig, ) { - const projectMap = await loadImportMap("/workspace/site", adapter); + const projectMap = await loadImportMap("/workspace/site", adapter, validatedConfig); const overrides = { imports: { "@app/": "/_vf_modules/app/", @@ -162,7 +164,9 @@ export async function resolveReact( `mergeImportMaps` accepts maps as separate arguments. Later maps win for exact keys, while scoped maps are merged per scope. `loadImportMap` applies framework defaults, project `deno.json`, and Veryfront configuration in that order and -then enforces the framework React mappings. +then enforces the framework React mappings. Its optional third argument accepts +an already validated request configuration; without it, the loader discovers +the project configuration from the project path. ## Operational contracts diff --git a/src/modules/import-map/loader-primordial-poisoning.worker.ts b/src/modules/import-map/loader-primordial-poisoning.worker.ts new file mode 100644 index 0000000000..903b3f3a78 --- /dev/null +++ b/src/modules/import-map/loader-primordial-poisoning.worker.ts @@ -0,0 +1,88 @@ +import type { VeryfrontConfig } from "#veryfront/config"; +import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; +import { loadImportMap } from "./loader.ts"; + +const denoJson = JSON.stringify({ + imports: { + "deno-only": "https://example.com/deno.ts", + package: "https://example.com/deno-package.ts", + }, +}); +const adapter = { + fs: { + getAdapterType: () => "VeryfrontFSAdapter", + getUnderlyingAdapter: () => ({}), + isVeryfrontAdapter: () => true, + isMultiProjectMode: () => false, + readFile: () => denoJson, + }, + env: { get: () => undefined }, +} as unknown as RuntimeAdapter; +const config = { + resolve: { + importMap: { + imports: { package: "npm:package@1.0.0" }, + }, + }, +} as VeryfrontConfig; + +async function runRegression() { + const original = { + arrayFilter: Array.prototype.filter, + arrayMap: Array.prototype.map, + arrayPush: Array.prototype.push, + jsonParse: JSON.parse, + objectAssign: Object.assign, + objectEntries: Object.entries, + objectFromEntries: Object.fromEntries, + stringIndexOf: String.prototype.indexOf, + stringSlice: String.prototype.slice, + stringSplit: String.prototype.split, + stringStartsWith: String.prototype.startsWith, + }; + const poisoned = () => { + throw new Error("poisoned primordial"); + }; + let loaded: Awaited> | undefined; + try { + Reflect.set(Array.prototype, "filter", poisoned); + Reflect.set(Array.prototype, "map", poisoned); + Reflect.set(Array.prototype, "push", poisoned); + Reflect.set(JSON, "parse", poisoned); + Reflect.set(Object, "assign", poisoned); + Reflect.set(Object, "entries", poisoned); + Reflect.set(Object, "fromEntries", poisoned); + Reflect.set(String.prototype, "indexOf", poisoned); + Reflect.set(String.prototype, "slice", poisoned); + Reflect.set(String.prototype, "split", poisoned); + Reflect.set(String.prototype, "startsWith", poisoned); + loaded = await loadImportMap("/project", adapter, config); + } finally { + Reflect.set(Array.prototype, "filter", original.arrayFilter); + Reflect.set(Array.prototype, "map", original.arrayMap); + Reflect.set(Array.prototype, "push", original.arrayPush); + Reflect.set(JSON, "parse", original.jsonParse); + Reflect.set(Object, "assign", original.objectAssign); + Reflect.set(Object, "entries", original.objectEntries); + Reflect.set(Object, "fromEntries", original.objectFromEntries); + Reflect.set(String.prototype, "indexOf", original.stringIndexOf); + Reflect.set(String.prototype, "slice", original.stringSlice); + Reflect.set(String.prototype, "split", original.stringSplit); + Reflect.set(String.prototype, "startsWith", original.stringStartsWith); + } + + return { + denoOnly: loaded?.imports?.["deno-only"], + package: loaded?.imports?.package, + react: loaded?.imports?.react, + }; +} + +try { + postMessage({ ok: true, result: await runRegression() }); +} catch (error) { + postMessage({ + ok: false, + error: error instanceof Error ? (error.stack ?? error.message) : String(error), + }); +} diff --git a/src/modules/import-map/loader.test.ts b/src/modules/import-map/loader.test.ts index 0b1fbdb619..e40ffe73cf 100644 --- a/src/modules/import-map/loader.test.ts +++ b/src/modules/import-map/loader.test.ts @@ -1,6 +1,8 @@ import "#veryfront/schemas/_test-setup.ts"; -import { assert, assertEquals, assertExists } from "#veryfront/testing/assert.ts"; +import { assert, assertEquals, assertExists, assertRejects } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; +import { VeryfrontError } from "#veryfront/errors"; +import type { VeryfrontConfig } from "#veryfront/config"; import { createMockAdapter } from "#veryfront/platform/adapters/mock.ts"; import { loadImportMap } from "./loader.ts"; @@ -21,6 +23,108 @@ describe("modules/import-map/loader", () => { assertExists(imports); assert("react" in imports, "should include react mapping"); assert("react-dom" in imports, "should include react-dom mapping"); + assert("react/" in imports, "should include authoritative react prefix mapping"); + assert("react-dom/" in imports, "should include authoritative react-dom prefix mapping"); + }); + + it("keeps React package prefixes authoritative over project mappings", async () => { + const adapter = createMockAdapter(); + const config = { + resolve: { + importMap: { + imports: { + "react/": "https://project.example/react/", + "react/compiler-runtime": "https://project.example/react-compiler.js", + "react-dom/": "https://project.example/react-dom/", + "react-dom/static": "https://project.example/react-dom-static.js", + }, + scopes: { + "/app/": { + react: "https://project.example/scoped-react.js", + "react-dom/static": "https://project.example/scoped-react-dom.js", + "veryfront/router": "https://project.example/scoped-router.js", + package: "https://project.example/package.js", + }, + }, + }, + }, + } as VeryfrontConfig; + + const { imports, scopes } = await loadImportMap("/any-project", adapter, config); + + assertExists(imports); + assertExists(scopes); + assertEquals(imports["react/"]?.startsWith("https://esm.sh/react@"), true); + assertEquals(imports["react-dom/"]?.startsWith("https://esm.sh/react-dom@"), true); + assertEquals(imports["react/"]?.endsWith("/"), true); + assertEquals(imports["react-dom/"]?.endsWith("/"), true); + assertEquals(imports["react/compiler-runtime"], undefined); + assertEquals(imports["react-dom/static"], undefined); + assertEquals(scopes["/app/"]?.react, undefined); + assertEquals(scopes["/app/"]?.["react-dom/static"], undefined); + assertEquals(scopes["/app/"]?.["veryfront/router"], undefined); + assertEquals(scopes["/app/"]?.package, "https://project.example/package.js"); + }); + + it("throws the registered import-map error for malformed explicit config", async () => { + const adapter = createMockAdapter(); + const config = { + resolve: { + importMap: { + imports: { package: 42 }, + }, + }, + } as unknown as VeryfrontConfig; + + const error = await assertRejects(() => loadImportMap("/any-project", adapter, config)); + + assert(error instanceof VeryfrontError); + assertEquals(error.slug, "import-map-invalid"); + assertEquals(error.detail, "Veryfront config resolve importMap is invalid"); + assertEquals(error.detail?.includes("42"), false); + }); + + it("ignores extra explicit config import-map metadata without invoking accessors", async () => { + const adapter = createMockAdapter(); + let metadataCalls = 0; + const importMap = { + imports: { package: "https://project.example/package.js" }, + }; + Object.defineProperty(importMap, "metadata", { + enumerable: true, + get() { + metadataCalls++; + return { source: "project" }; + }, + }); + const config = { + resolve: { importMap }, + } as VeryfrontConfig; + + const { imports } = await loadImportMap("/any-project", adapter, config); + + assertEquals(imports?.package, "https://project.example/package.js"); + assertEquals(metadataCalls, 0); + }); + + it("rejects config accessors without invoking project code", async () => { + const adapter = createMockAdapter(); + let accessorCalls = 0; + const config = {} as VeryfrontConfig; + Object.defineProperty(config, "resolve", { + enumerable: true, + get() { + accessorCalls++; + return {}; + }, + }); + + const error = await assertRejects(() => loadImportMap("/any-project", adapter, config)); + + assert(error instanceof VeryfrontError); + assertEquals(error.slug, "import-map-invalid"); + assertEquals(error.detail, "Veryfront config cannot contain accessor properties"); + assertEquals(accessorCalls, 0); }); it("should include veryfront framework mappings", async () => { @@ -79,6 +183,20 @@ describe("modules/import-map/loader", () => { assert("react" in imports, "should include default react"); }); + it("probes a canonical deno.json path when the project path has a trailing slash", async () => { + const adapter = createMockAdapter(); + adapter.fs.files.set( + "/trailing-slash-project/deno.json", + JSON.stringify({ + imports: { "project-package": "https://esm.sh/project-package@1" }, + }), + ); + + const { imports } = await loadImportMap("/trailing-slash-project/", adapter); + + assertEquals(imports?.["project-package"], "https://esm.sh/project-package@1"); + }); + it("should use esm.sh URLs for React", async () => { const adapter = createMockAdapter(); const { imports } = await loadImportMap("/any-project", adapter); @@ -184,5 +302,34 @@ describe("modules/import-map/loader", () => { assert(!("relative" in appScope), "relative path in scope should be filtered"); assert("absolute" in appScope, "absolute path in scope should be kept"); }); + + it("keeps dependency resolution deterministic after primordial poisoning", async () => { + const worker = new Worker( + new URL("./loader-primordial-poisoning.worker.ts", import.meta.url), + { type: "module" }, + ); + try { + const result = await new Promise<{ + denoOnly: string | undefined; + package: string | undefined; + react: string | undefined; + }>((resolve, reject) => { + worker.onmessage = (event) => { + const message = event.data as + | { ok: true; result: Parameters[0] } + | { ok: false; error: string }; + if (message.ok) resolve(message.result); + else reject(new Error(message.error)); + }; + worker.onerror = (event) => reject(event.error ?? new Error(event.message)); + }); + + assertEquals(result.denoOnly, "https://example.com/deno.ts"); + assertEquals(result.package, "https://esm.sh/package@1.0.0?target=es2022"); + assert(result.react?.includes("esm.sh")); + } finally { + worker.terminate(); + } + }); }); }); diff --git a/src/modules/import-map/loader.ts b/src/modules/import-map/loader.ts index 56fa4b460c..ae950f77f0 100644 --- a/src/modules/import-map/loader.ts +++ b/src/modules/import-map/loader.ts @@ -1,176 +1,296 @@ -import { rendererLogger as logger } from "#veryfront/utils"; -import { dirname, join } from "#veryfront/compat/path/index.ts"; +import { getConfig, type VeryfrontConfig } from "#veryfront/config"; +import { IMPORT_MAP_INVALID, isVeryfrontError } from "#veryfront/errors"; +import { withSpan } from "#veryfront/observability/tracing/otlp-setup.ts"; import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; import { isVirtualFilesystem } from "#veryfront/platform/adapters/fs/wrapper.ts"; -import { getConfig } from "#veryfront/config"; -import type { ImportMapConfig } from "./types.ts"; +import { snapshotImportMap } from "#veryfront/transforms/pipeline/cache-identity.ts"; +import { getReactImportMap } from "#veryfront/transforms/esm/react-cdn.ts"; +import { rendererLogger as logger } from "#veryfront/utils"; +import { dirname, join } from "#veryfront/compat/path/index.ts"; import { getDefaultImportMap } from "./default-import-map.ts"; import { mergeImportMaps } from "./merger.ts"; -import { withSpan } from "#veryfront/observability/tracing/otlp-setup.ts"; -import { getReactImportMap } from "#veryfront/transforms/esm/react-cdn.ts"; +import type { ImportMapConfig } from "./types.ts"; -function normalizeImportMapForRuntime(importMap: ImportMapConfig): ImportMapConfig { - const normalizeValue = (value: string): string => { - if (!value.startsWith("npm:")) return value; +// A hosted project can execute in this realm before a later request loads its +// import map. Capture the primitives and framework-owned maps used to select +// executable modules so replacing shared globals cannot redirect resolution. +const JSONParse = JSON.parse; +const ArrayIsArray = Array.isArray; +const ObjectCreate = Object.create; +const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; +const ObjectPrototype = Object.prototype; +const ObjectGetPrototypeOf = Object.getPrototypeOf; +const ReflectApply = Reflect.apply; +const ReflectOwnKeys = Reflect.ownKeys; +const StringPrototypeIndexOf = String.prototype.indexOf; +const StringPrototypeSlice = String.prototype.slice; +const StringPrototypeStartsWith = String.prototype.startsWith; - // Convert npm: specifiers to esm.sh URLs (should not happen with new code) - const spec = value.slice(4); - const [base, query] = spec.split("?"); - const url = `https://esm.sh/${base}`; +const DEFAULT_IMPORT_MAP = snapshotImportMap(getDefaultImportMap()); +const REACT_IMPORTS = snapshotImportMap({ imports: getReactImportMap() }).imports!; - return query ? `${url}?${query}` : `${url}?target=es2022`; - }; +function stringStartsWith(value: string, prefix: string): boolean { + return ReflectApply(StringPrototypeStartsWith, value, [prefix]) as boolean; +} + +function stringSlice(value: string, start: number, end?: number): string { + return ReflectApply( + StringPrototypeSlice, + value, + end === undefined ? [start] : [start, end], + ) as string; +} + +function hasOwn(object: object, key: PropertyKey): boolean { + return ReflectApply(ObjectPrototypeHasOwnProperty, object, [key]) as boolean; +} + +function isFrameworkOwnedSpecifier(specifier: string): boolean { + return specifier === "react" || specifier === "react-dom" || + stringStartsWith(specifier, "react/") || + stringStartsWith(specifier, "react-dom/") || + stringStartsWith(specifier, "veryfront/"); +} + +function removeFrameworkOwnedMappings(record: Record): void { + const keys = ReflectOwnKeys(record); + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + if (typeof key === "string" && isFrameworkOwnedSpecifier(key)) { + delete record[key]; + } + } +} + +function readOwnDataProperty( + value: object, + key: PropertyKey, + label: string, +): unknown { + const descriptor = ObjectGetOwnPropertyDescriptor(value, key); + if (!descriptor) return undefined; + if (!hasOwn(descriptor, "value")) { + throw IMPORT_MAP_INVALID.create({ + detail: `${label} cannot contain accessor properties`, + }); + } + return descriptor.value; +} + +function assertPlainObject(value: unknown, label: string): asserts value is object { + if (value === null || typeof value !== "object" || ArrayIsArray(value)) { + throw IMPORT_MAP_INVALID.create({ detail: `${label} must be a plain object` }); + } + const prototype = ObjectGetPrototypeOf(value); + if (prototype !== ObjectPrototype && prototype !== null) { + throw IMPORT_MAP_INVALID.create({ detail: `${label} must be a plain object` }); + } +} + +function readEmbeddedImportMap( + container: unknown, + label: string, +): ImportMapConfig | null { + assertPlainObject(container, label); + const imports = readOwnDataProperty(container, "imports", label); + const scopes = readOwnDataProperty(container, "scopes", label); + if (imports === undefined && scopes === undefined) return null; + return snapshotImportMap({ + imports: imports ?? ObjectCreate(null), + scopes: scopes ?? ObjectCreate(null), + }); +} + +function copyFilteredRecord( + record: Readonly>, + normalizeNpm: boolean, +): Record { + const result = ObjectCreate(null) as Record; + const keys = ReflectOwnKeys(record); + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + if (typeof key !== "string") continue; + const descriptor = ObjectGetOwnPropertyDescriptor(record, key); + if (!descriptor?.enumerable || !hasOwn(descriptor, "value")) continue; + const value = descriptor.value as string; + if (stringStartsWith(value, "./") || stringStartsWith(value, "../")) continue; + result[key] = normalizeNpm ? normalizeImportValue(value) : value; + } + return result; +} - let imports = importMap.imports - ? Object.fromEntries(Object.entries(importMap.imports).map(([k, v]) => [k, normalizeValue(v)])) - : undefined; - - const scopes = importMap.scopes - ? Object.fromEntries( - Object.entries(importMap.scopes).map(([scope, mappings]) => [ - scope, - Object.fromEntries(Object.entries(mappings).map(([k, v]) => [k, normalizeValue(v)])), - ]), - ) - : undefined; - - // Override React mappings AFTER all other processing to ensure single instance. - // Remove any "react/" prefix match since we have explicit mappings. - if (imports) { - const veryfrontSsrMap = Object.fromEntries( - Object.entries(getDefaultImportMap().imports ?? {}).filter(([key]) => - key.startsWith("veryfront/") - ), +function filterRelativePaths(importMap: ImportMapConfig): ImportMapConfig { + const exact = snapshotImportMap(importMap); + const imports = copyFilteredRecord(exact.imports ?? ObjectCreate(null), false); + const scopes = ObjectCreate(null) as Record>; + const exactScopes = exact.scopes ?? ObjectCreate(null); + const scopeKeys = ReflectOwnKeys(exactScopes); + for (let index = 0; index < scopeKeys.length; index++) { + const scope = scopeKeys[index]; + if (typeof scope !== "string") continue; + const descriptor = ObjectGetOwnPropertyDescriptor(exactScopes, scope); + if (!descriptor?.enumerable || !hasOwn(descriptor, "value")) continue; + scopes[scope] = copyFilteredRecord( + descriptor.value as Readonly>, + false, ); - const reactMap = getReactImportMap(); - delete imports["react/"]; - imports = { ...imports, ...veryfrontSsrMap, ...reactMap }; } + return snapshotImportMap({ imports, scopes }); +} - return { imports, scopes }; +function normalizeImportValue(value: string): string { + if (!stringStartsWith(value, "npm:")) return value; + const specifier = stringSlice(value, 4); + const queryIndex = ReflectApply(StringPrototypeIndexOf, specifier, ["?"]) as number; + const base = queryIndex < 0 ? specifier : stringSlice(specifier, 0, queryIndex); + const query = queryIndex < 0 ? "" : stringSlice(specifier, queryIndex + 1); + const url = `https://esm.sh/${base}`; + return query ? `${url}?${query}` : `${url}?target=es2022`; +} + +function normalizeImportMapForRuntime(importMap: ImportMapConfig): ImportMapConfig { + const exact = snapshotImportMap(importMap); + const imports = copyFilteredRecord(exact.imports ?? ObjectCreate(null), true); + const scopes = ObjectCreate(null) as Record>; + const exactScopes = exact.scopes ?? ObjectCreate(null); + const scopeKeys = ReflectOwnKeys(exactScopes); + for (let index = 0; index < scopeKeys.length; index++) { + const scope = scopeKeys[index]; + if (typeof scope !== "string") continue; + const descriptor = ObjectGetOwnPropertyDescriptor(exactScopes, scope); + if (!descriptor?.enumerable || !hasOwn(descriptor, "value")) continue; + scopes[scope] = copyFilteredRecord( + descriptor.value as Readonly>, + true, + ); + removeFrameworkOwnedMappings(scopes[scope]); + } + + // Framework and React mappings are authoritative, guaranteeing one React + // instance and preventing exact, prefix, or scoped project overrides from + // redirecting core code. + removeFrameworkOwnedMappings(imports); + const defaultImports = DEFAULT_IMPORT_MAP.imports ?? ObjectCreate(null); + const defaultKeys = ReflectOwnKeys(defaultImports); + for (let index = 0; index < defaultKeys.length; index++) { + const key = defaultKeys[index]; + if (typeof key !== "string" || !stringStartsWith(key, "veryfront/")) continue; + const descriptor = ObjectGetOwnPropertyDescriptor(defaultImports, key); + if (descriptor?.enumerable && hasOwn(descriptor, "value")) { + imports[key] = descriptor.value as string; + } + } + const reactKeys = ReflectOwnKeys(REACT_IMPORTS); + for (let index = 0; index < reactKeys.length; index++) { + const key = reactKeys[index]; + if (typeof key !== "string") continue; + const descriptor = ObjectGetOwnPropertyDescriptor(REACT_IMPORTS, key); + if (descriptor?.enumerable && hasOwn(descriptor, "value")) { + imports[key] = descriptor.value as string; + } + } + return snapshotImportMap({ imports, scopes }); } async function getRuntimeAdapter(adapter?: RuntimeAdapter): Promise { if (adapter) return adapter; - const { runtime } = await import("#veryfront/platform/adapters/detect.ts"); return runtime.get(); } -/** - * Filter out relative paths from import map entries. - * - * Relative paths (./foo, ../bar) in deno.json are for Deno's native module resolution. - * They can't work in the browser/SSR context where we serve modules via /_vf_modules/. - * The default import map has correct absolute paths like /_vf_modules/_veryfront/... - */ -function filterRelativePaths(imports: Record): Record { - return Object.fromEntries( - Object.entries(imports).filter(([, value]) => - !value.startsWith("./") && !value.startsWith("../") - ), - ); -} - async function loadDenoJsonImportMap( startPath: string, adapter: RuntimeAdapter, ): Promise { - // For virtual filesystems (API-backed), only check project root - // Virtual filesystems use relative paths, not absolute local paths + const readMap = async (path: string): Promise => { + const content = await adapter.fs.readFile(path); + const parsed = ReflectApply(JSONParse, JSON, [content]) as unknown; + const map = readEmbeddedImportMap(parsed, "deno.json"); + return map ? filterRelativePaths(map) : null; + }; + if (isVirtualFilesystem(adapter.fs)) { try { - const content = await adapter.fs.readFile("deno.json"); - const config = JSON.parse(content); - - if (config.imports || config.scopes) { - logger.debug("Loaded import map from deno.json (virtual filesystem)"); - const imports = config.imports ? filterRelativePaths(config.imports) : {}; - const scopes = config.scopes - ? Object.fromEntries( - Object.entries(config.scopes as Record>).map( - ([scope, mappings]) => [scope, filterRelativePaths(mappings)], - ), - ) - : {}; - return { imports, scopes }; - } + const map = await readMap("deno.json"); + if (map) logger.debug("Loaded import map from deno.json (virtual filesystem)"); + return map; } catch (_) { - /* expected: deno.json not found in virtual filesystem */ + return null; } - return null; } - // For local filesystems, walk up directory tree let currentPath = startPath; - while (currentPath !== "/" && currentPath !== "") { const denoJsonPath = join(currentPath, "deno.json"); - try { - const content = await adapter.fs.readFile(denoJsonPath); - const config = JSON.parse(content); - - if (config.imports || config.scopes) { + const map = await readMap(denoJsonPath); + if (map) { logger.debug(`Loaded import map from ${denoJsonPath}`); - const imports = config.imports ? filterRelativePaths(config.imports) : {}; - const scopes = config.scopes - ? Object.fromEntries( - Object.entries(config.scopes as Record>).map( - ([scope, mappings]) => [scope, filterRelativePaths(mappings)], - ), - ) - : {}; - return { imports, scopes }; + return map; } } catch (_) { - /* expected: deno.json not found in this directory, continue searching */ + // A missing or invalid deno.json does not override framework defaults. } - const parent = dirname(currentPath); if (parent === currentPath) break; currentPath = parent; } - return null; } +function getConfigImportMap(config: VeryfrontConfig): ImportMapConfig | null { + try { + assertPlainObject(config, "Veryfront config"); + const resolve = readOwnDataProperty(config, "resolve", "Veryfront config"); + if (resolve === undefined) return null; + assertPlainObject(resolve, "Veryfront config resolve"); + const importMap = readOwnDataProperty( + resolve, + "importMap", + "Veryfront config resolve", + ); + if (importMap === undefined || importMap === null) return null; + const embedded = readEmbeddedImportMap( + importMap, + "Veryfront config resolve importMap", + ); + return embedded ?? snapshotImportMap({}); + } catch (error) { + if (isVeryfrontError(error)) throw error; + throw IMPORT_MAP_INVALID.create({ + detail: "Veryfront config resolve importMap is invalid", + }); + } +} + export function loadImportMap( startPath: string, adapter?: RuntimeAdapter, + config?: VeryfrontConfig, ): Promise { return withSpan( "modules.importMap.load", async () => { const runtimeAdapter = await getRuntimeAdapter(adapter); - - // First, load import map from deno.json (if exists) const denoJsonMap = await loadDenoJsonImportMap(startPath, runtimeAdapter); - - // Then, try to get config's import map let configMap: ImportMapConfig | null = null; - try { - const cfg = await getConfig(startPath, runtimeAdapter); - const importMap = cfg?.resolve?.importMap; - if (importMap && typeof importMap === "object") { - configMap = { - imports: importMap.imports ?? {}, - scopes: importMap.scopes ?? {}, - }; + if (config) { + configMap = getConfigImportMap(config); + } else { + try { + const loadedConfig = await getConfig(startPath, runtimeAdapter); + if (loadedConfig) configMap = getConfigImportMap(loadedConfig); + } catch (_) { + // A missing or invalid optional config does not override safe defaults. } - } catch (_) { - /* expected: config not found or invalid, continue without it */ } - // Merge: defaults < deno.json < config - // If both deno.json and config have import maps, config takes precedence for overlapping keys - // but deno.json's unique keys (especially scopes) are preserved const merged = mergeImportMaps( - getDefaultImportMap(), + DEFAULT_IMPORT_MAP, denoJsonMap ?? { imports: {}, scopes: {} }, configMap ?? { imports: {}, scopes: {} }, ); - return normalizeImportMapForRuntime(merged); }, { "importMap.startPath": startPath }, diff --git a/src/modules/import-map/merger.test.ts b/src/modules/import-map/merger.test.ts index efeda7c38f..fe20a78ade 100644 --- a/src/modules/import-map/merger.test.ts +++ b/src/modules/import-map/merger.test.ts @@ -2,6 +2,7 @@ import "#veryfront/schemas/_test-setup.ts"; import { assertEquals } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { mergeImportMaps } from "./merger.ts"; +import type { ImportMapConfig } from "./types.ts"; describe("modules/import-map/merger", () => { describe("mergeImportMaps", () => { @@ -52,5 +53,58 @@ describe("modules/import-map/merger", () => { const result = mergeImportMaps({ imports: { a: "b" } }); assertEquals(result.imports?.a, "b"); }); + + it("should preserve compatibility with enumerable metadata fields", () => { + const map = { + imports: { a: "b" }, + metadata: { source: "project" }, + } as { imports: Record; metadata: { source: string } }; + + const result = mergeImportMaps(map); + + assertEquals(result.imports?.a, "b"); + }); + + it("rejects accessors after inherited descriptor poisoning without iterating keys", () => { + const originalArrayIterator = Array.prototype[Symbol.iterator]; + const originalValue = Object.getOwnPropertyDescriptor(Object.prototype, "value"); + let getterCalls = 0; + let accessorError: unknown; + const accessorMap = Object.defineProperty({}, "imports", { + enumerable: true, + get() { + getterCalls++; + return { accessed: "https://example.com/accessed.ts" }; + }, + }) as ImportMapConfig; + + try { + Reflect.set(Array.prototype, Symbol.iterator, function (this: unknown[]) { + if (this.length === 2 && this[0] === "imports" && this[1] === "scopes") { + return { next: () => ({ done: true, value: undefined }) }; + } + return Reflect.apply(originalArrayIterator, this, []); + }); + Object.defineProperty(Object.prototype, "value", { + configurable: true, + value: { poisoned: "https://example.com/poisoned.ts" }, + }); + + try { + mergeImportMaps(accessorMap); + } catch (error) { + accessorError = error; + } + const merged = mergeImportMaps({ imports: { safe: "https://example.com/safe.ts" } }); + assertEquals(merged.imports?.safe, "https://example.com/safe.ts"); + } finally { + Reflect.set(Array.prototype, Symbol.iterator, originalArrayIterator); + if (originalValue) Object.defineProperty(Object.prototype, "value", originalValue); + else Reflect.deleteProperty(Object.prototype, "value"); + } + + assertEquals(accessorError instanceof TypeError, true); + assertEquals(getterCalls, 0); + }); }); }); diff --git a/src/modules/import-map/merger.ts b/src/modules/import-map/merger.ts index 6c1bfaa60e..0969af7ef1 100644 --- a/src/modules/import-map/merger.ts +++ b/src/modules/import-map/merger.ts @@ -1,19 +1,74 @@ +import { snapshotImportMap } from "#veryfront/transforms/pipeline/cache-identity.ts"; import type { ImportMapConfig } from "./types.ts"; -export function mergeImportMaps(...maps: ImportMapConfig[]): ImportMapConfig { - const imports: Record = {}; - const scopes: Record> = {}; +// Import maps can be merged after project code has executed in this realm. +// Capture the small set of primitives used here and only read validated, +// descriptor-snapshotted records. +const ObjectCreate = Object.create; +const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; +const ReflectApply = Reflect.apply; +const ReflectOwnKeys = Reflect.ownKeys; +const IntrinsicTypeError = TypeError; + +function hasOwn(object: object, key: PropertyKey): boolean { + return ReflectApply(ObjectPrototypeHasOwnProperty, object, [key]) as boolean; +} - for (const map of maps) { - if (map.imports) Object.assign(imports, map.imports); +function copySnapshotField( + input: ImportMapConfig, + map: ImportMapConfig, + key: "imports" | "scopes", +): void { + const descriptor = ObjectGetOwnPropertyDescriptor(map, key); + if (!descriptor) return; + if (!hasOwn(descriptor, "value")) { + throw new IntrinsicTypeError(`Import map ${key} cannot contain accessor properties`); + } + input[key] = descriptor.value; +} + +function snapshotMergeInput(map: ImportMapConfig): ImportMapConfig { + const input = ObjectCreate(null) as ImportMapConfig; + copySnapshotField(input, map, "imports"); + copySnapshotField(input, map, "scopes"); + return snapshotImportMap(input); +} + +function copyStringRecord( + target: Record, + source: Readonly>, +): void { + const keys = ReflectOwnKeys(source); + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + if (typeof key !== "string") continue; + const descriptor = ObjectGetOwnPropertyDescriptor(source, key); + if (descriptor?.enumerable && hasOwn(descriptor, "value")) { + target[key] = descriptor.value as string; + } + } +} + +export function mergeImportMaps(...maps: ImportMapConfig[]): ImportMapConfig { + const imports = ObjectCreate(null) as Record; + const scopes = ObjectCreate(null) as Record>; - if (!map.scopes) continue; + for (let index = 0; index < maps.length; index++) { + const map = snapshotMergeInput(maps[index]!); + copyStringRecord(imports, map.imports ?? ObjectCreate(null)); - for (const [scope, scopeImports] of Object.entries(map.scopes)) { - scopes[scope] ??= {}; - Object.assign(scopes[scope], scopeImports); + const mapScopes = map.scopes ?? ObjectCreate(null); + const scopeKeys = ReflectOwnKeys(mapScopes); + for (let scopeIndex = 0; scopeIndex < scopeKeys.length; scopeIndex++) { + const scope = scopeKeys[scopeIndex]; + if (typeof scope !== "string") continue; + const descriptor = ObjectGetOwnPropertyDescriptor(mapScopes, scope); + if (!descriptor?.enumerable || !hasOwn(descriptor, "value")) continue; + const target = scopes[scope] ??= ObjectCreate(null) as Record; + copyStringRecord(target, descriptor.value as Readonly>); } } - return { imports, scopes }; + return snapshotImportMap({ imports, scopes }); } diff --git a/src/modules/import-map/preloader-primordial-poisoning.worker.ts b/src/modules/import-map/preloader-primordial-poisoning.worker.ts new file mode 100644 index 0000000000..85ed7c8eae --- /dev/null +++ b/src/modules/import-map/preloader-primordial-poisoning.worker.ts @@ -0,0 +1,167 @@ +import type { VeryfrontConfig } from "#veryfront/config"; +import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; +import { ImportMapPreloader } from "./preloader.ts"; + +const adapter = { + fs: {}, + env: {}, +} as unknown as RuntimeAdapter; +const configA = { + resolve: { + importMap: { + imports: { package: "https://example.com/package-a.ts" }, + }, + }, +} as VeryfrontConfig; +const configB = { + resolve: { + importMap: { + imports: { package: "https://example.com/package-b.ts" }, + }, + }, +} as VeryfrontConfig; + +async function runPoisoningRegression() { + const original = { + arrayMap: Array.prototype.map, + arrayPush: Array.prototype.push, + arraySort: Array.prototype.sort, + dateNow: Date.now, + jsonStringify: JSON.stringify, + map: Map, + mapClear: Map.prototype.clear, + mapDelete: Map.prototype.delete, + mapForEach: Map.prototype.forEach, + mapGet: Map.prototype.get, + mapSet: Map.prototype.set, + mapSize: Object.getOwnPropertyDescriptor(Map.prototype, "size")!, + mathMin: Math.min, + numberIsFinite: Number.isFinite, + numberIsSafeInteger: Number.isSafeInteger, + objectEntries: Object.entries, + promise: Promise, + promiseResolve: Promise.resolve, + set: Set, + setAdd: Set.prototype.add, + setDelete: Set.prototype.delete, + setForEach: Set.prototype.forEach, + setSize: Object.getOwnPropertyDescriptor(Set.prototype, "size")!, + }; + const poisoned = () => { + throw new Error("poisoned primordial"); + }; + let first: Awaited> | undefined; + let firstAgain: Awaited> | undefined; + let second: Awaited> | undefined; + let evicted: + | Awaited> + | undefined; + let loads = 0; + + try { + Reflect.set(Array.prototype, "map", poisoned); + Reflect.set(Array.prototype, "push", poisoned); + Reflect.set(Array.prototype, "sort", poisoned); + Reflect.set(Date, "now", poisoned); + Reflect.set(JSON, "stringify", poisoned); + Reflect.set(globalThis, "Map", class PoisonedMap {}); + Reflect.set(original.map.prototype, "clear", poisoned); + Reflect.set(original.map.prototype, "delete", poisoned); + Reflect.set(original.map.prototype, "forEach", poisoned); + Reflect.set(original.map.prototype, "get", poisoned); + Reflect.set(original.map.prototype, "set", poisoned); + Object.defineProperty(original.map.prototype, "size", { + configurable: true, + get: poisoned, + }); + Reflect.set(Math, "min", poisoned); + Reflect.set(Number, "isFinite", poisoned); + Reflect.set(Number, "isSafeInteger", poisoned); + Reflect.set(Object, "entries", poisoned); + Reflect.set(globalThis, "Promise", class PoisonedPromise {}); + Reflect.set(original.promise, "resolve", poisoned); + Reflect.set(globalThis, "Set", class PoisonedSet {}); + Reflect.set(original.set.prototype, "add", poisoned); + Reflect.set(original.set.prototype, "delete", poisoned); + Reflect.set(original.set.prototype, "forEach", poisoned); + Object.defineProperty(original.set.prototype, "size", { + configurable: true, + get: poisoned, + }); + + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 1, + ttlMs: 1_000, + loadImportMap: async (_path, _adapter, config) => ({ + imports: { + loaded: String(++loads), + package: config?.resolve?.importMap?.imports?.package ?? "", + }, + }), + }); + const contextA = { + contentSourceId: "source", + config: configA, + projectDir: "/project", + }; + const contextB = { + contentSourceId: "source", + config: configB, + projectDir: "/project", + }; + + first = await preloader.preload("/project", adapter, "project", contextA); + firstAgain = await preloader.preload( + "/project", + adapter, + "project", + contextA, + ); + second = await preloader.preload("/project", adapter, "project", contextB); + evicted = await preloader.getCached("project", contextA); + } finally { + Reflect.set(Array.prototype, "map", original.arrayMap); + Reflect.set(Array.prototype, "push", original.arrayPush); + Reflect.set(Array.prototype, "sort", original.arraySort); + Reflect.set(Date, "now", original.dateNow); + Reflect.set(JSON, "stringify", original.jsonStringify); + Reflect.set(original.map.prototype, "clear", original.mapClear); + Reflect.set(original.map.prototype, "delete", original.mapDelete); + Reflect.set(original.map.prototype, "forEach", original.mapForEach); + Reflect.set(original.map.prototype, "get", original.mapGet); + Reflect.set(original.map.prototype, "set", original.mapSet); + Object.defineProperty(original.map.prototype, "size", original.mapSize); + Reflect.set(Math, "min", original.mathMin); + Reflect.set(Number, "isFinite", original.numberIsFinite); + Reflect.set(Number, "isSafeInteger", original.numberIsSafeInteger); + Reflect.set(Object, "entries", original.objectEntries); + Reflect.set(original.promise, "resolve", original.promiseResolve); + Reflect.set(original.set.prototype, "add", original.setAdd); + Reflect.set(original.set.prototype, "delete", original.setDelete); + Reflect.set(original.set.prototype, "forEach", original.setForEach); + Object.defineProperty(original.set.prototype, "size", original.setSize); + Reflect.set(globalThis, "Map", original.map); + Reflect.set(globalThis, "Promise", original.promise); + Reflect.set(globalThis, "Set", original.set); + } + + return { + firstLoaded: first?.imports?.loaded, + firstSame: firstAgain === first, + secondLoaded: second?.imports?.loaded, + secondPackage: second?.imports?.package, + evicted: evicted === undefined, + loads, + }; +} + +try { + const result = await runPoisoningRegression(); + postMessage({ ok: true, result }); +} catch (error) { + postMessage({ + ok: false, + error: error instanceof Error ? (error.stack ?? error.message) : String(error), + }); +} diff --git a/src/modules/import-map/preloader.test.ts b/src/modules/import-map/preloader.test.ts index 1cc6270d05..9e093c0b46 100644 --- a/src/modules/import-map/preloader.test.ts +++ b/src/modules/import-map/preloader.test.ts @@ -1,14 +1,30 @@ import "#veryfront/schemas/_test-setup.ts"; -import { assertEquals } from "#veryfront/testing/assert.ts"; +import { + assertEquals, + assertRejects, + assertStrictEquals, + assertStringIncludes, + assertThrows, +} from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; -import { clearImportMapCache, getCachedImportMap, preloadImportMap } from "./preloader.ts"; +import { + clearImportMapCache, + getCachedImportMap, + ImportMapPreloader, + preloadImportMap, +} from "./preloader.ts"; +import { validateVeryfrontConfig, type VeryfrontConfig } from "#veryfront/config"; import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; +import { MAX_TIMER_DELAY_MS } from "#veryfront/utils/constants/limits.ts"; +import type { ImportMapConfig } from "./types.ts"; function createMinimalAdapter(): RuntimeAdapter { return { fs: { readFile: () => { - throw new Error("not found"); + const error = new Error("not found") as Error & { code: string }; + error.code = "ENOENT"; + throw error; }, writeFile: () => {}, exists: () => false, @@ -22,7 +38,27 @@ function createMinimalAdapter(): RuntimeAdapter { env: { get: () => undefined, }, - } as RuntimeAdapter; + } as unknown as RuntimeAdapter; +} + +function createDeferred() { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, reject, resolve }; +} + +async function waitForLoadCount( + loads: readonly unknown[], + expected: number, +): Promise { + for (let attempt = 0; attempt < 100 && loads.length < expected; attempt++) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + assertEquals(loads.length, expected); } describe("modules/import-map/preloader", () => { @@ -44,7 +80,7 @@ describe("modules/import-map/preloader", () => { const map1 = await preloadImportMap("/test-cache-same", adapter); const map2 = await preloadImportMap("/test-cache-same", adapter); - assertEquals(map1, map2); + assertStrictEquals(map1, map2); }); it("should cache different projects independently", async () => { @@ -57,6 +93,515 @@ describe("modules/import-map/preloader", () => { assertEquals(typeof result1, "object"); assertEquals(typeof result2, "object"); }); + + it("isolates cache entries when one project source receives a changed config map", async () => { + clearImportMapCache(); + const adapter = createMinimalAdapter(); + const firstConfig = validateVeryfrontConfig({ + resolve: { + importMap: { + imports: { package: "https://example.com/package-v1.ts" }, + }, + }, + }); + const secondConfig = validateVeryfrontConfig({ + resolve: { + importMap: { + imports: { package: "https://example.com/package-v2.ts" }, + }, + }, + }); + const firstContext = { + contentSourceId: "release-1", + config: firstConfig, + }; + + const first = await preloadImportMap( + "/shared-project", + adapter, + "project-1", + firstContext, + ); + const firstAgain = await preloadImportMap( + "/shared-project", + adapter, + "project-1", + firstContext, + ); + const changed = await preloadImportMap( + "/shared-project", + adapter, + "project-1", + { + contentSourceId: "release-1", + config: secondConfig, + }, + ); + + assertStrictEquals(first, firstAgain); + assertEquals(first.imports?.package, "https://example.com/package-v1.ts"); + assertEquals(changed.imports?.package, "https://example.com/package-v2.ts"); + assertEquals(first === changed, false); + }); + + it("ignores extra config import-map metadata without invoking accessors", async () => { + clearImportMapCache(); + const adapter = createMinimalAdapter(); + let metadataCalls = 0; + const importMap = { + imports: { package: "https://example.com/package.ts" }, + }; + Object.defineProperty(importMap, "metadata", { + enumerable: true, + get() { + metadataCalls++; + return { source: "project" }; + }, + }); + const config = { resolve: { importMap } } as VeryfrontConfig; + + const result = await preloadImportMap( + "/metadata-project", + adapter, + "metadata-project", + { config }, + ); + + assertEquals(result.imports?.package, "https://example.com/package.ts"); + assertEquals(metadataCalls, 0); + }); + + it("rejects accessor-backed config import-map fields without invoking them", async () => { + clearImportMapCache(); + const adapter = createMinimalAdapter(); + let importsCalls = 0; + const importMap = {}; + Object.defineProperty(importMap, "imports", { + enumerable: true, + get() { + importsCalls++; + return { package: "https://example.com/package.ts" }; + }, + }); + const config = { resolve: { importMap } } as VeryfrontConfig; + + await assertRejects( + () => + preloadImportMap( + "/accessor-import-map-project", + adapter, + "accessor-import-map-project", + { config }, + ), + TypeError, + "imports cannot be an accessor", + ); + assertEquals(importsCalls, 0); + }); + + it("binds variant identity and loading to one pre-await config snapshot", async () => { + const adapter = createMinimalAdapter(); + const releaseLoader = createDeferred(); + let loads = 0; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 2, + ttlMs: 1_000, + loadImportMap: async (_path, _adapter, config) => { + loads += 1; + await releaseLoader.promise; + return { + imports: { + package: config?.resolve?.importMap?.imports?.package ?? "", + }, + }; + }, + }); + const config = { + resolve: { + importMap: { + imports: { package: "https://example.com/package-a.ts" }, + }, + }, + } as VeryfrontConfig; + const context = { contentSourceId: "release", config }; + + const firstPromise = preloader.preload( + "/atomic-project", + adapter, + "atomic-project", + context, + ); + const mutableImports = config.resolve?.importMap?.imports as Record< + string, + string + >; + mutableImports.package = "https://example.com/package-b.ts"; + releaseLoader.resolve(); + const first = await firstPromise; + + const originalContext = { + contentSourceId: "release", + config: validateVeryfrontConfig({ + resolve: { + importMap: { + imports: { package: "https://example.com/package-a.ts" }, + }, + }, + }), + }; + const cachedOriginal = await preloader.preload( + "/atomic-project", + adapter, + "atomic-project", + originalContext, + ); + const changed = await preloader.preload( + "/atomic-project", + adapter, + "atomic-project", + context, + ); + + assertEquals(first.imports?.package, "https://example.com/package-a.ts"); + assertStrictEquals(cachedOriginal, first); + assertEquals(changed.imports?.package, "https://example.com/package-b.ts"); + assertEquals(loads, 2); + }); + + it("isolates cache entries across content sources with the same validated config", async () => { + clearImportMapCache(); + const adapter = createMinimalAdapter(); + const config = validateVeryfrontConfig({ + resolve: { + importMap: { + imports: { package: "https://example.com/package.ts" }, + }, + }, + }); + + const release = await preloadImportMap( + "/shared-project", + adapter, + "project-1", + { contentSourceId: "release-1", config }, + ); + const branch = await preloadImportMap( + "/shared-project", + adapter, + "project-1", + { contentSourceId: "branch-main", config }, + ); + + assertEquals(release === branch, false); + }); + + it("isolates project roots that share one project ID and content source", async () => { + const adapter = createMinimalAdapter(); + let loads = 0; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 2, + ttlMs: 1_000, + loadImportMap: async (projectDir) => ({ + imports: { projectDir, load: String(++loads) }, + }), + }); + const context = { contentSourceId: "release-1" }; + + const first = await preloader.preload( + "/releases/first", + adapter, + "project-1", + context, + ); + const second = await preloader.preload( + "/releases/second", + adapter, + "project-1", + context, + ); + + assertEquals(first.imports?.projectDir, "/releases/first"); + assertEquals(second.imports?.projectDir, "/releases/second"); + assertEquals(loads, 2); + }); + + it("rejects accessor-backed request context without invoking it", async () => { + const adapter = createMinimalAdapter(); + let getterCalls = 0; + const context = Object.defineProperty({}, "contentSourceId", { + enumerable: true, + get() { + getterCalls++; + return "poisoned"; + }, + }); + + await assertRejects( + () => + preloadImportMap( + "/accessor-context", + adapter, + "accessor-context", + context, + ), + TypeError, + "cannot be an accessor", + ); + assertEquals(getterCalls, 0); + }); + + it("rejects accessor-backed config after inherited descriptor poisoning", async () => { + const adapter = createMinimalAdapter(); + const originalValue = Object.getOwnPropertyDescriptor(Object.prototype, "value"); + let getterCalls = 0; + let accessorError: unknown; + const context = Object.defineProperty({}, "config", { + enumerable: true, + get() { + getterCalls++; + return undefined; + }, + }); + + try { + Object.defineProperty(Object.prototype, "value", { + configurable: true, + value: { + resolve: { + importMap: { + imports: { poisoned: "https://example.com/poisoned.ts" }, + }, + }, + }, + }); + try { + await preloadImportMap( + "/inherited-value-accessor-context", + adapter, + "inherited-value-accessor-context", + context, + ); + } catch (error) { + accessorError = error; + } + } finally { + if (originalValue) Object.defineProperty(Object.prototype, "value", originalValue); + else Reflect.deleteProperty(Object.prototype, "value"); + } + + assertEquals(accessorError instanceof TypeError, true); + assertStringIncludes((accessorError as Error).message, "cannot be an accessor"); + assertEquals(getterCalls, 0); + }); + + it("rejects non-object config context values", async () => { + const adapter = createMinimalAdapter(); + + for (const config of [null, false, 0, "invalid"]) { + await assertRejects( + () => + preloadImportMap( + "/invalid-config-context", + adapter, + `invalid-config-${String(config)}`, + { config } as never, + ), + TypeError, + "Import-map config must be an object", + ); + } + }); + + it("snapshots and deep-freezes loader output before publishing it", async () => { + const adapter = createMinimalAdapter(); + const loadedMap = { + imports: { package: "https://example.com/package-v1.ts" }, + scopes: { + "https://example.com/": { + scoped: "https://example.com/scoped-v1.ts", + }, + }, + }; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 1, + ttlMs: 1_000, + loadImportMap: async () => loadedMap, + }); + + const published = await preloader.preload( + "/immutable-loader-output", + adapter, + "immutable-loader-output", + ); + loadedMap.imports.package = "https://example.com/package-mutated.ts"; + loadedMap.scopes["https://example.com/"].scoped = "https://example.com/scoped-mutated.ts"; + + assertEquals(published === loadedMap, false); + assertEquals(Object.isFrozen(published), true); + assertEquals(Object.isFrozen(published.imports), true); + assertEquals(Object.isFrozen(published.scopes), true); + assertEquals( + Object.isFrozen(published.scopes?.["https://example.com/"]), + true, + ); + assertThrows( + () => { + published.imports!.package = "https://example.com/caller-mutation.ts"; + }, + TypeError, + ); + assertEquals( + published.imports?.package, + "https://example.com/package-v1.ts", + ); + assertEquals( + published.scopes?.["https://example.com/"]?.scoped, + "https://example.com/scoped-v1.ts", + ); + assertEquals( + await preloader.getCached("immutable-loader-output", { + projectDir: "/immutable-loader-output", + }), + published, + ); + assertEquals( + await preloader.preload( + "/immutable-loader-output", + adapter, + "immutable-loader-output", + ), + published, + ); + }); + + it("can isolate the same project id by explicit project directory context", async () => { + const adapter = createMinimalAdapter(); + let loads = 0; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 2, + loadImportMap: async () => ({ + imports: { loaded: String(++loads) }, + }), + }); + + const first = await preloader.preload("/release-a", adapter, "project", { + projectDir: "/release-a", + contentSourceId: "source", + }); + const second = await preloader.preload("/release-b", adapter, "project", { + projectDir: "/release-b", + contentSourceId: "source", + }); + + assertEquals(first.imports?.loaded, "1"); + assertEquals(second.imports?.loaded, "2"); + assertEquals( + await preloader.getCached("project", { + projectDir: "/release-a", + contentSourceId: "source", + }), + first, + ); + assertEquals( + await preloader.getCached("project", { + projectDir: "/release-b", + contentSourceId: "source", + }), + second, + ); + }); + + it("falls back to the only retained variant for project-id cache lookups", async () => { + const adapter = createMinimalAdapter(); + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 2, + loadImportMap: async () => ({ + imports: { loaded: "single" }, + }), + }); + + const loaded = await preloader.preload("/release-a", adapter, "project", { + projectDir: "/release-a", + contentSourceId: "source-a", + }); + + assertEquals(await preloader.getCached("project"), loaded); + assertEquals( + await preloader.getCached("project", { contentSourceId: "source-a" }), + undefined, + ); + }); + + it("does not guess a project-id cache lookup across multiple variants", async () => { + const adapter = createMinimalAdapter(); + let loads = 0; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 2, + loadImportMap: async () => ({ + imports: { loaded: String(++loads) }, + }), + }); + + await preloader.preload("/release-a", adapter, "project", { + projectDir: "/release-a", + contentSourceId: "source-a", + }); + await preloader.preload("/release-b", adapter, "project", { + projectDir: "/release-b", + contentSourceId: "source-b", + }); + + assertEquals(await preloader.getCached("project"), undefined); + }); + + it("rejects malformed loader output before publication and permits retry", async () => { + const adapter = createMinimalAdapter(); + let loads = 0; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 1, + ttlMs: 1_000, + loadImportMap: async () => { + loads += 1; + if (loads === 1) { + return { + imports: { package: 42 }, + } as unknown as ImportMapConfig; + } + return { + imports: { package: "https://example.com/recovered.ts" }, + }; + }, + }); + + await assertRejects( + () => + preloader.preload( + "/invalid-loader-output", + adapter, + "invalid-loader-output", + ), + TypeError, + "must be a string", + ); + const recovered = await preloader.preload( + "/invalid-loader-output", + adapter, + "invalid-loader-output", + ); + + assertEquals( + recovered.imports?.package, + "https://example.com/recovered.ts", + ); + assertEquals(loads, 2); + }); }); describe("getCachedImportMap", () => { @@ -78,6 +623,759 @@ describe("modules/import-map/preloader", () => { assertEquals(typeof cached, "object"); assertEquals(cached !== undefined, true); }); + + it("rejects accessor-backed projectDir after inherited descriptor poisoning", async () => { + const originalValue = Object.getOwnPropertyDescriptor(Object.prototype, "value"); + let getterCalls = 0; + let accessorError: unknown; + const context = Object.defineProperty({}, "projectDir", { + enumerable: true, + get() { + getterCalls++; + return "/accessed-project"; + }, + }); + + try { + Object.defineProperty(Object.prototype, "value", { + configurable: true, + value: "/inherited-project", + }); + try { + await getCachedImportMap("inherited-project", context); + } catch (error) { + accessorError = error; + } + } finally { + if (originalValue) Object.defineProperty(Object.prototype, "value", originalValue); + else Reflect.deleteProperty(Object.prototype, "value"); + } + + assertEquals(accessorError instanceof TypeError, true); + assertStringIncludes((accessorError as Error).message, "cannot be an accessor"); + assertEquals(getterCalls, 0); + }); + + it("preserves project-id lookup compatibility only for one unambiguous variant", async () => { + const preloader = new ImportMapPreloader({ + loadImportMap: () => + Promise.resolve({ + imports: { package: "https://example.com/package.ts" }, + }), + }); + const adapter = createMinimalAdapter(); + + await preloader.preload("/release/project", adapter, "project-id", { + contentSourceId: "release-1", + }); + + const cached = await preloader.getCached("project-id"); + assertEquals(cached?.imports?.package, "https://example.com/package.ts"); + + await preloader.preload("/branch/project", adapter, "project-id", { + contentSourceId: "branch-1", + }); + assertEquals(await preloader.getCached("project-id"), undefined); + assertEquals( + (await preloader.getCached("project-id", { + projectDir: "/release/project", + contentSourceId: "release-1", + }))?.imports?.package, + "https://example.com/package.ts", + ); + }); + }); + + describe("bounded cache lifecycle", () => { + function createTestPreloader(input: { + maxProjects?: number; + maxVariantsPerProject?: number; + ttlMs?: number; + now?: () => number; + }) { + let loads = 0; + const preloader = new ImportMapPreloader({ + ...input, + loadImportMap: () => + Promise.resolve({ + imports: { loaded: String(++loads) }, + }), + }); + return { preloader, getLoads: () => loads }; + } + + it("rejects load timeouts that cannot be represented by the host timer", () => { + assertThrows( + () => + new ImportMapPreloader({ + loadTimeoutMs: MAX_TIMER_DELAY_MS + 1, + }), + RangeError, + `loadTimeoutMs must not exceed ${MAX_TIMER_DELAY_MS}`, + ); + }); + + it("evicts the least-recently-used variant within one project", async () => { + const adapter = createMinimalAdapter(); + const { preloader } = createTestPreloader({ + maxProjects: 2, + maxVariantsPerProject: 2, + ttlMs: 1_000, + }); + const projectDir = "/bounded-variants"; + const projectId = "project-1"; + const sourceA = { contentSourceId: "source-a", projectDir }; + const sourceB = { contentSourceId: "source-b", projectDir }; + const sourceC = { contentSourceId: "source-c", projectDir }; + + await preloader.preload(projectDir, adapter, projectId, sourceA); + await preloader.preload(projectDir, adapter, projectId, sourceB); + await preloader.getCached(projectId, sourceA); + await preloader.preload(projectDir, adapter, projectId, sourceC); + + assertEquals(await preloader.getCached(projectId, sourceA) !== undefined, true); + assertEquals(await preloader.getCached(projectId, sourceB), undefined); + assertEquals(await preloader.getCached(projectId, sourceC) !== undefined, true); + }); + + it("evicts the least-recently-used project bucket", async () => { + const adapter = createMinimalAdapter(); + const { preloader } = createTestPreloader({ + maxProjects: 2, + maxVariantsPerProject: 2, + ttlMs: 1_000, + }); + + await preloader.preload("/project-a", adapter, "project-a"); + await preloader.preload("/project-b", adapter, "project-b"); + await preloader.getCached("project-a", { projectDir: "/project-a" }); + await preloader.preload("/project-c", adapter, "project-c"); + + assertEquals( + await preloader.getCached("project-a", { projectDir: "/project-a" }) !== undefined, + true, + ); + assertEquals( + await preloader.getCached("project-b", { projectDir: "/project-b" }), + undefined, + ); + assertEquals( + await preloader.getCached("project-c", { projectDir: "/project-c" }) !== undefined, + true, + ); + }); + + it("expires settled entries against an injected clock and reloads them", async () => { + const adapter = createMinimalAdapter(); + let now = 1_000; + const { preloader, getLoads } = createTestPreloader({ + maxProjects: 2, + maxVariantsPerProject: 2, + ttlMs: 100, + now: () => now, + }); + const context = { contentSourceId: "source-a", projectDir: "/ttl-project" }; + + const first = await preloader.preload("/ttl-project", adapter, "ttl-project", context); + now = 1_099; + assertEquals(await preloader.getCached("ttl-project", context), first); + now = 1_100; + assertEquals(await preloader.getCached("ttl-project", context), undefined); + + const reloaded = await preloader.preload( + "/ttl-project", + adapter, + "ttl-project", + context, + ); + assertEquals(getLoads(), 2); + assertEquals(reloaded === first, false); + }); + + it("publishes one authoritative replacement on direct preload after expiry", async () => { + const adapter = createMinimalAdapter(); + let now = 1_000; + const { preloader, getLoads } = createTestPreloader({ + maxProjects: 1, + maxVariantsPerProject: 1, + ttlMs: 100, + now: () => now, + }); + const context = { + contentSourceId: "source-a", + projectDir: "/direct-expiry", + }; + + const expired = await preloader.preload( + "/direct-expiry", + adapter, + "direct-expiry", + context, + ); + now = 1_100; + const replacement = await preloader.preload( + "/direct-expiry", + adapter, + "direct-expiry", + context, + ); + const cachedReplacement = await preloader.preload( + "/direct-expiry", + adapter, + "direct-expiry", + context, + ); + + assertEquals(replacement === expired, false); + assertStrictEquals(cachedReplacement, replacement); + assertEquals(getLoads(), 2); + }); + + it("deduplicates concurrent direct refreshes at capacity after expiry", async () => { + const adapter = createMinimalAdapter(); + let now = 1_000; + const loads: Array>> = []; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 1, + ttlMs: 100, + now: () => now, + loadImportMap: () => { + const load = createDeferred(); + loads.push(load); + return load.promise; + }, + }); + const context = { + contentSourceId: "source-a", + projectDir: "/concurrent-direct-expiry", + }; + + const initialPromise = preloader.preload( + "/concurrent-direct-expiry", + adapter, + "concurrent-direct-expiry", + context, + ); + await waitForLoadCount(loads, 1); + loads[0]!.resolve({ imports: { loaded: "initial" } }); + const initial = await initialPromise; + + now = 1_100; + const replacementPromise = preloader.preload( + "/concurrent-direct-expiry", + adapter, + "concurrent-direct-expiry", + context, + ); + await waitForLoadCount(loads, 2); + const duplicatePromise = preloader.preload( + "/concurrent-direct-expiry", + adapter, + "concurrent-direct-expiry", + context, + ); + const cachedPromise = preloader.getCached( + "concurrent-direct-expiry", + context, + ); + + await Promise.resolve(); + assertEquals(loads.length, 2); + loads[1]!.resolve({ imports: { loaded: "replacement" } }); + const [replacement, duplicate, cached] = await Promise.all([ + replacementPromise, + duplicatePromise, + cachedPromise, + ]); + + assertEquals(replacement === initial, false); + assertEquals(duplicate, replacement); + assertStrictEquals(cached, replacement); + assertEquals( + await preloader.preload( + "/concurrent-direct-expiry", + adapter, + "concurrent-direct-expiry", + context, + ), + replacement, + ); + assertEquals(loads.length, 2); + }); + + it("removes a settled entry when the injected clock throws", async () => { + const adapter = createMinimalAdapter(); + let clockReads = 0; + let loads = 0; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 1, + ttlMs: 100, + now: () => { + clockReads += 1; + if (clockReads === 3) throw new Error("clock unavailable"); + return 1_000; + }, + loadImportMap: async () => ({ + imports: { loaded: String(++loads) }, + }), + }); + const context = { + contentSourceId: "source-a", + projectDir: "/throwing-clock", + }; + + const first = await preloader.preload( + "/throwing-clock", + adapter, + "throwing-clock", + context, + ); + + assertEquals(first.imports?.loaded, "1"); + assertEquals( + await preloader.getCached("throwing-clock", context), + undefined, + ); + + const second = await preloader.preload( + "/throwing-clock", + adapter, + "throwing-clock", + context, + ); + assertEquals(second.imports?.loaded, "2"); + assertEquals(loads, 2); + }); + + it("preserves explicit project invalidation in a bounded cache", async () => { + const adapter = createMinimalAdapter(); + const { preloader } = createTestPreloader({ + maxProjects: 2, + maxVariantsPerProject: 2, + ttlMs: 1_000, + }); + + await preloader.preload("/project-a", adapter, "project-a"); + await preloader.preload("/project-b", adapter, "project-b"); + preloader.clear("project-a"); + + assertEquals( + await preloader.getCached("project-a", { projectDir: "/project-a" }), + undefined, + ); + assertEquals( + await preloader.getCached("project-b", { projectDir: "/project-b" }) !== undefined, + true, + ); + }); + + it("does not publish pre-clear work after identity hashing resumes", async () => { + const adapter = createMinimalAdapter(); + let loads = 0; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 1, + ttlMs: 1_000, + loadImportMap: async () => ({ + imports: { loaded: String(++loads) }, + }), + }); + const context = { contentSourceId: "source-a", projectDir: "/project-a" }; + + const preClear = preloader.preload( + "/project-a", + adapter, + "project-a", + context, + ); + preloader.clear("project-a"); + + const postClear = preloader.preload( + "/project-a", + adapter, + "project-a", + context, + ); + const staleResult = await preClear; + assertEquals(staleResult.imports?.loaded, "1"); + assertEquals(Object.isFrozen(staleResult), true); + assertEquals(Object.isFrozen(staleResult.imports), true); + assertThrows( + () => { + staleResult.imports!.loaded = "caller-mutation"; + }, + TypeError, + ); + const reloaded = await postClear; + assertEquals(reloaded.imports?.loaded, "2"); + assertStrictEquals(await preloader.getCached("project-a", context), reloaded); + }); + + it("does not publish pre-clear work into a new global generation", async () => { + const adapter = createMinimalAdapter(); + let loads = 0; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 1, + ttlMs: 1_000, + loadImportMap: async () => ({ + imports: { loaded: String(++loads) }, + }), + }); + const context = { contentSourceId: "source-a", projectDir: "/project-a" }; + + const preClear = preloader.preload( + "/project-a", + adapter, + "project-a", + context, + ); + preloader.clear(); + + assertEquals((await preClear).imports?.loaded, "1"); + assertEquals(await preloader.getCached("project-a", context), undefined); + }); + + it("waits for in-flight project capacity instead of failing renders", async () => { + const adapter = createMinimalAdapter(); + const loads: Array>> = []; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 1, + ttlMs: 1_000, + loadImportMap: () => { + const load = createDeferred(); + loads.push(load); + return load.promise; + }, + }); + + const first = preloader.preload("/project-a", adapter, "project-a"); + await Promise.resolve(); + const sameKey = preloader.preload("/project-a", adapter, "project-a"); + const queued = preloader.preload("/project-b", adapter, "project-b"); + await waitForLoadCount(loads, 1); + assertEquals(loads.length, 1); + + loads[0]!.resolve({ imports: { source: "a" } }); + const firstResult = await first; + assertEquals(firstResult.imports?.source, "a"); + assertStrictEquals(await sameKey, firstResult); + + await waitForLoadCount(loads, 2); + loads[1]!.resolve({ imports: { source: "b" } }); + assertEquals((await queued).imports?.source, "b"); + }); + + it("waits for in-flight variant capacity across explicit invalidation", async () => { + const adapter = createMinimalAdapter(); + const loads: Array>> = []; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 1, + ttlMs: 1_000, + loadImportMap: () => { + const load = createDeferred(); + loads.push(load); + return load.promise; + }, + }); + const sourceA = { contentSourceId: "source-a", projectDir: "/project" }; + const sourceB = { contentSourceId: "source-b", projectDir: "/project" }; + + const first = preloader.preload("/project", adapter, "project", sourceA); + const queued = preloader.preload("/project", adapter, "project", sourceB); + preloader.clear("project"); + await waitForLoadCount(loads, 1); + assertEquals(loads.length, 1); + + loads[0]!.resolve({ imports: { source: "a" } }); + await first; + await waitForLoadCount(loads, 2); + loads[1]!.resolve({ imports: { source: "b" } }); + assertEquals((await queued).imports?.source, "b"); + assertEquals(await preloader.getCached("project", sourceA), undefined); + assertEquals((await preloader.getCached("project", sourceB))?.imports?.source, "b"); + }); + + it("returns undefined from getCached when identity capacity is occupied", async () => { + const adapter = createMinimalAdapter(); + const loads: Array>> = []; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 1, + ttlMs: 1_000, + loadImportMap: () => { + const load = createDeferred(); + loads.push(load); + return load.promise; + }, + }); + + const first = preloader.preload("/project", adapter, "project", { + contentSourceId: "source-a", + }); + await waitForLoadCount(loads, 1); + + assertEquals( + await preloader.getCached("project", { contentSourceId: "source-b" }), + undefined, + ); + + loads[0]!.resolve({ imports: { source: "a" } }); + await first; + }); + + it("counts cleared underlying work against the total project bound", async () => { + const adapter = createMinimalAdapter(); + const loads: Array>> = []; + const preloader = new ImportMapPreloader({ + maxProjects: 2, + maxVariantsPerProject: 1, + ttlMs: 1_000, + loadTimeoutMs: 1_000, + loadImportMap: () => { + const load = createDeferred(); + loads.push(load); + return load.promise; + }, + }); + + const cached = preloader.preload("/cached", adapter, "cached"); + await waitForLoadCount(loads, 1); + loads[0]!.resolve({ imports: { source: "cached" } }); + await cached; + + const cleared = preloader.preload("/project-a", adapter, "project-a"); + await waitForLoadCount(loads, 2); + preloader.clear("project-a"); + + const activeB = preloader.preload("/project-b", adapter, "project-b"); + await waitForLoadCount(loads, 3); + const queuedD = preloader.preload("/project-d", adapter, "project-d"); + await Promise.resolve(); + assertEquals(loads.length, 3); + + loads[1]!.resolve({ imports: { source: "late-a" } }); + assertEquals((await cleared).imports?.source, "late-a"); + await waitForLoadCount(loads, 4); + loads[2]!.resolve({ imports: { source: "b" } }); + loads[3]!.resolve({ imports: { source: "d" } }); + assertEquals((await activeB).imports?.source, "b"); + assertEquals((await queuedD).imports?.source, "d"); + }); + + it("keeps timed-out underlying work scoped to its project capacity", async () => { + const adapter = createMinimalAdapter(); + const loads: Array>> = []; + const preloader = new ImportMapPreloader({ + maxProjects: 2, + maxVariantsPerProject: 1, + ttlMs: 1_000, + loadTimeoutMs: 20, + loadImportMap: () => { + const load = createDeferred(); + loads.push(load); + return load.promise; + }, + }); + + const hungA = preloader.preload("/hung-a", adapter, "hung-a"); + await waitForLoadCount(loads, 1); + await assertRejects( + () => hungA, + RangeError, + "load timed out", + ); + await assertRejects( + () => preloader.preload("/hung-a", adapter, "hung-a"), + RangeError, + "capacity wait timed out", + ); + assertEquals(loads.length, 1); + + const hungB = preloader.preload("/hung-b", adapter, "hung-b"); + await waitForLoadCount(loads, 2); + const nextBlockedByCapacity = preloader.preload("/next", adapter, "next"); + await new Promise((resolve) => setTimeout(resolve, 0)); + assertEquals(loads.length, 2); + + loads[1]!.resolve({ imports: { source: "b" } }); + assertEquals((await hungB).imports?.source, "b"); + loads[0]!.resolve({ imports: { source: "late" } }); + await waitForLoadCount(loads, 3); + loads[2]!.resolve({ imports: { source: "next" } }); + assertEquals((await nextBlockedByCapacity).imports?.source, "next"); + + await Promise.resolve(); + const sameProjectRecovered = preloader.preload("/hung-a", adapter, "hung-a"); + await waitForLoadCount(loads, 4); + loads[3]!.resolve({ imports: { source: "hung-recovered" } }); + assertEquals((await sameProjectRecovered).imports?.source, "hung-recovered"); + }); + + it("reserves project capacity before an invalidated load times out", async () => { + const adapter = createMinimalAdapter(); + const loads: Array>> = []; + const preloader = new ImportMapPreloader({ + maxProjects: 2, + maxVariantsPerProject: 2, + ttlMs: 1_000, + loadTimeoutMs: 1_000, + loadImportMap: () => { + const load = createDeferred(); + loads.push(load); + return load.promise; + }, + }); + + const invalidated = preloader.preload("/project-a", adapter, "project-a"); + await waitForLoadCount(loads, 1); + preloader.clear("project-a"); + + const second = preloader.preload("/project-b", adapter, "project-b"); + await waitForLoadCount(loads, 2); + const queued = preloader.preload("/project-c", adapter, "project-c"); + for (let attempt = 0; attempt < 10; attempt++) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + assertEquals(loads.length, 2); + + loads[1]!.resolve({ imports: { source: "b" } }); + assertEquals((await second).imports?.source, "b"); + await waitForLoadCount(loads, 3); + loads[2]!.resolve({ imports: { source: "c" } }); + assertEquals((await queued).imports?.source, "c"); + + loads[0]!.resolve({ imports: { source: "a" } }); + assertEquals((await invalidated).imports?.source, "a"); + }); + + it("counts timed-out work against the total project bound", async () => { + const adapter = createMinimalAdapter(); + const loads: Array>> = []; + const preloader = new ImportMapPreloader({ + maxProjects: 2, + maxVariantsPerProject: 1, + ttlMs: 1_000, + loadTimeoutMs: 100, + loadImportMap: () => { + const load = createDeferred(); + loads.push(load); + return load.promise; + }, + }); + + const timedOutA = preloader.preload("/project-a", adapter, "project-a"); + await waitForLoadCount(loads, 1); + await assertRejects(() => timedOutA, RangeError, "load timed out"); + + const activeB = preloader.preload("/project-b", adapter, "project-b"); + await waitForLoadCount(loads, 2); + const queuedC = preloader.preload("/project-c", adapter, "project-c"); + await Promise.resolve(); + assertEquals(loads.length, 2); + + loads[0]!.resolve({ imports: { source: "late-a" } }); + await waitForLoadCount(loads, 3); + loads[1]!.resolve({ imports: { source: "b" } }); + loads[2]!.resolve({ imports: { source: "c" } }); + assertEquals((await activeB).imports?.source, "b"); + assertEquals((await queuedC).imports?.source, "c"); + }); + + it("does not miss capacity released before a waiter observes its signal", async () => { + const adapter = createMinimalAdapter(); + const loads: Array>> = []; + let releaseDuringAdmission = false; + let admissionClockReads = 0; + let clock = 0; + const preloader = new ImportMapPreloader({ + maxProjects: 1, + maxVariantsPerProject: 2, + ttlMs: 1_000, + loadTimeoutMs: 1_000, + now: () => { + if (releaseDuringAdmission && admissionClockReads++ === 0) { + loads[0]!.resolve({ imports: { source: "a" } }); + } + return ++clock; + }, + loadImportMap: () => { + const load = createDeferred(); + loads.push(load); + return load.promise; + }, + }); + + const first = preloader.preload("/project", adapter, "project", { + contentSourceId: "source-a", + }); + const unrelated = preloader.preload("/project", adapter, "project", { + contentSourceId: "source-b", + }); + await waitForLoadCount(loads, 2); + + releaseDuringAdmission = true; + const queued = preloader.preload("/project", adapter, "project", { + contentSourceId: "source-c", + }); + + await waitForLoadCount(loads, 3); + assertEquals((await first).imports?.source, "a"); + loads[2]!.resolve({ imports: { source: "c" } }); + assertEquals((await queued).imports?.source, "c"); + loads[1]!.resolve({ imports: { source: "b" } }); + assertEquals((await unrelated).imports?.source, "b"); + }); + + it("keeps variant identity and capacity deterministic after primordial poisoning", async () => { + const worker = new Worker( + new URL("./preloader-primordial-poisoning.worker.ts", import.meta.url), + { type: "module" }, + ); + try { + const result = await new Promise<{ + firstLoaded: string | undefined; + firstSame: boolean; + secondLoaded: string | undefined; + secondPackage: string | undefined; + evicted: boolean; + loads: number; + }>((resolve, reject) => { + const timeoutId = setTimeout( + () => reject(new Error("primordial poisoning worker timed out")), + 30_000, + ); + worker.onmessage = (event) => { + clearTimeout(timeoutId); + const message = event.data as + | { ok: true; result: Parameters[0] } + | { ok: false; error: string }; + if (message.ok) resolve(message.result); + else reject(new Error(message.error)); + }; + worker.onerror = (event) => { + clearTimeout(timeoutId); + reject(event.error ?? new Error(event.message)); + }; + }); + + assertEquals(result.firstLoaded, "1"); + assertEquals(result.firstSame, true); + assertEquals(result.secondLoaded, "2"); + assertEquals( + result.secondPackage, + "https://example.com/package-b.ts", + ); + assertEquals(result.evicted, true); + assertEquals(result.loads, 2); + } finally { + worker.terminate(); + } + }); }); describe("clearImportMapCache", () => { diff --git a/src/modules/import-map/preloader.ts b/src/modules/import-map/preloader.ts index 8866d1e066..158fdb26fe 100644 --- a/src/modules/import-map/preloader.ts +++ b/src/modules/import-map/preloader.ts @@ -1,47 +1,1163 @@ +import type { VeryfrontConfig } from "#veryfront/config"; import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; +import { primordialArraySort as arraySort } from "#veryfront/platform/compat/primordials/array.ts"; +import { snapshotImportMap } from "#veryfront/transforms/pipeline/cache-identity.ts"; +import { MAX_TIMER_DELAY_MS } from "#veryfront/utils/constants/limits.ts"; +import { computeHash } from "#veryfront/utils/hash-utils.ts"; +import { rendererLogger } from "#veryfront/utils"; import type { ImportMapConfig } from "./types.ts"; import { loadImportMap } from "./loader.ts"; -const importMapCache = new Map>(); +export interface PreloadImportMapContext { + /** Immutable content source selected for this render (release, branch, or environment). */ + contentSourceId?: string; + /** Config already validated for the authenticated request. */ + config?: VeryfrontConfig; + /** Project root used by cache-inspection callers when the cache key is a project ID. */ + projectDir?: string; +} -export function preloadImportMap( - projectDir: string, - adapter: RuntimeAdapter, - projectId?: string, -): Promise { - const cacheKey = projectId ?? projectDir; - const cached = importMapCache.get(cacheKey); - if (cached) return cached; +const IMPORT_MAP_CACHE_IDENTITY_NAMESPACE = "veryfront:preloaded-import-map:v2"; +const DEFAULT_MAX_IMPORT_MAP_PROJECTS = 512; +const DEFAULT_MAX_IMPORT_MAP_VARIANTS_PER_PROJECT = 16; +const DEFAULT_IMPORT_MAP_TTL_MS = 10 * 60 * 1_000; +const DEFAULT_IMPORT_MAP_LOAD_TIMEOUT_MS = 30_000; +const logger = rendererLogger.component("import-map-preloader"); - const promise = loadImportMap(projectDir, adapter); - importMapCache.set(cacheKey, promise); +// Project code can execute in the same realm before a later request reaches +// this cache. Capture every primitive used for identity, admission, and +// settlement so replacing shared built-ins cannot redirect dependency graphs. +const DateNow = Date.now; +const IntrinsicMap = Map; +const IntrinsicPerformance = performance; +const IntrinsicPromise = Promise; +const IntrinsicRangeError = RangeError; +const IntrinsicSet = Set; +const IntrinsicWeakSet = WeakSet; +const IntrinsicTypeError = TypeError; +const JSONStringify = JSON.stringify; +const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; +const MapPrototypeClear = Map.prototype.clear; +const MapPrototypeDelete = Map.prototype.delete; +const MapPrototypeForEach = Map.prototype.forEach; +const MapPrototypeGet = Map.prototype.get; +const MapPrototypeSet = Map.prototype.set; +const MapPrototypeSize = ObjectGetOwnPropertyDescriptor(Map.prototype, "size")! + .get!; +const MathMin = Math.min; +const NUMBER_MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER; +const NumberIsFinite = Number.isFinite; +const NumberIsSafeInteger = Number.isSafeInteger; +const ObjectEntries = Object.entries; +const ObjectFreeze = Object.freeze; +const PromisePrototypeThen = Promise.prototype.then; +const PromiseResolve = Promise.resolve; +const PerformanceNow = IntrinsicPerformance.now; +const ReflectApply = Reflect.apply; +const SetPrototypeAdd = Set.prototype.add; +const SetPrototypeDelete = Set.prototype.delete; +const SetPrototypeSize = ObjectGetOwnPropertyDescriptor(Set.prototype, "size")! + .get!; +const WeakSetPrototypeAdd = WeakSet.prototype.add; +const WeakSetPrototypeHas = WeakSet.prototype.has; +const SetTimeout = setTimeout; +const ClearTimeout = clearTimeout; - promise.catch(() => { - importMapCache.delete(cacheKey); +function monotonicNow(): number { + return ReflectApply(PerformanceNow, IntrinsicPerformance, []) as number; +} + +function hasOwn(object: object, key: PropertyKey): boolean { + return ReflectApply(ObjectPrototypeHasOwnProperty, object, [key]) as boolean; +} + +function snapshotEmbeddedImportMap(value: unknown): ImportMapConfig { + if (value === undefined || value === null) return snapshotImportMap({}); + if (typeof value !== "object") return snapshotImportMap(value); + + const importsDescriptor = ObjectGetOwnPropertyDescriptor(value, "imports"); + if (importsDescriptor && !hasOwn(importsDescriptor, "value")) { + throw new IntrinsicTypeError( + "Import-map config resolve.importMap imports cannot be an accessor", + ); + } + const scopesDescriptor = ObjectGetOwnPropertyDescriptor(value, "scopes"); + if (scopesDescriptor && !hasOwn(scopesDescriptor, "value")) { + throw new IntrinsicTypeError( + "Import-map config resolve.importMap scopes cannot be an accessor", + ); + } + + return snapshotImportMap({ + imports: importsDescriptor?.value ?? {}, + scopes: scopesDescriptor?.value ?? {}, }); +} - return promise; +function mapClear(map: Map): void { + ReflectApply(MapPrototypeClear, map, []); } -export async function getCachedImportMap( - cacheKey: string, -): Promise { - const cached = importMapCache.get(cacheKey); - if (!cached) return undefined; +function mapDelete(map: Map, key: K): boolean { + return ReflectApply(MapPrototypeDelete, map, [key]) as boolean; +} + +function mapForEach( + map: Map, + callback: (value: V, key: K) => void, +): void { + ReflectApply(MapPrototypeForEach, map, [callback]); +} + +function mapGet(map: Map, key: K): V | undefined { + return ReflectApply(MapPrototypeGet, map, [key]) as V | undefined; +} + +function mapSet(map: Map, key: K, value: V): void { + ReflectApply(MapPrototypeSet, map, [key, value]); +} + +function mapSize(map: Map): number { + return ReflectApply(MapPrototypeSize, map, []) as number; +} + +function promiseThen( + promise: Promise, + onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onRejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, +): Promise { + return ReflectApply(PromisePrototypeThen, promise, [ + onFulfilled, + onRejected, + ]) as Promise; +} + +function resolvedPromise(): Promise { + return ReflectApply(PromiseResolve, IntrinsicPromise, []) as Promise; +} + +function raceTwo(first: Promise, second: Promise): Promise { + return new IntrinsicPromise((resolve, reject) => { + promiseThen(first, resolve, reject); + promiseThen(second, resolve, reject); + }); +} + +function setAdd(set: Set, value: T): void { + ReflectApply(SetPrototypeAdd, set, [value]); +} + +function setDelete(set: Set, value: T): boolean { + return ReflectApply(SetPrototypeDelete, set, [value]) as boolean; +} + +function setSize(set: Set): number { + return ReflectApply(SetPrototypeSize, set, []) as number; +} + +function weakSetAdd(set: WeakSet, value: T): void { + ReflectApply(WeakSetPrototypeAdd, set, [value]); +} + +function weakSetHas(set: WeakSet, value: T): boolean { + return ReflectApply(WeakSetPrototypeHas, set, [value]) as boolean; +} + +interface CachedImportMap { + readonly promise: Promise; + /** Starts when the load settles; in-flight work is never expired mid-flight. */ + expiresAt: number | null; +} - try { - return await cached; - } catch (_) { - /* expected: cached import map promise may have been rejected */ - return undefined; +type ProjectImportMapCache = Map; + +interface ProjectImportMapState { + readonly variants: ProjectImportMapCache; + generation: object; + /** Hashes being computed or retained while their matching load is in flight. */ + readonly identityBuilds: Map>; +} + +interface CapacityChangeSignal { + promise: Promise; + resolve: () => void; +} + +function createGeneration(): object { + return ObjectFreeze({}); +} + +function createCapacityChangeSignal(): CapacityChangeSignal { + let resolve!: () => void; + const promise = new IntrinsicPromise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +export interface ImportMapPreloaderOptions { + /** Maximum tenant/project buckets retained by one preloader. */ + maxProjects?: number; + /** Maximum content-source/config variants retained for one project. */ + maxVariantsPerProject?: number; + /** Retention lifetime after a successful load. */ + ttlMs?: number; + /** Maximum caller wait for a load or for occupied capacity to settle. */ + loadTimeoutMs?: number; + /** Monotonic-enough clock seam; defaults to Date.now. */ + now?: () => number; + /** Loader seam for alternate runtimes and deterministic verification. */ + loadImportMap?: typeof loadImportMap; +} + +function compareEntries( + left: readonly [string, T], + right: readonly [string, U], +): number { + return left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0; +} + +/** + * Bind cache identity and loader input to one immutable import-map snapshot + * before the first async boundary. Validated config objects are caller-owned + * and need not remain unchanged while SHA-256 is being computed. + */ +function snapshotPreloadContext( + projectDir: string, + context?: PreloadImportMapContext, +): PreloadImportMapContext { + if (typeof projectDir !== "string") { + throw new IntrinsicTypeError("Import-map projectDir must be a string"); + } + if (!context) return ObjectFreeze({ projectDir }); + if (typeof context !== "object") { + throw new IntrinsicTypeError("Import-map context must be an object"); + } + const contentSourceDescriptor = ObjectGetOwnPropertyDescriptor( + context, + "contentSourceId", + ); + if (contentSourceDescriptor && !hasOwn(contentSourceDescriptor, "value")) { + throw new IntrinsicTypeError("Import-map contentSourceId cannot be an accessor"); + } + const contentSourceId = contentSourceDescriptor?.value; + if (contentSourceId !== undefined && typeof contentSourceId !== "string") { + throw new IntrinsicTypeError("Import-map contentSourceId must be a string"); + } + const configDescriptor = ObjectGetOwnPropertyDescriptor(context, "config"); + if (configDescriptor && !hasOwn(configDescriptor, "value")) { + throw new IntrinsicTypeError("Import-map config cannot be an accessor"); + } + const config = configDescriptor?.value as VeryfrontConfig | undefined; + if (config === undefined) return ObjectFreeze({ contentSourceId, projectDir }); + if (config === null || typeof config !== "object") { + throw new IntrinsicTypeError("Import-map config must be an object"); + } + const resolveDescriptor = ObjectGetOwnPropertyDescriptor(config, "resolve"); + if (resolveDescriptor && !hasOwn(resolveDescriptor, "value")) { + throw new IntrinsicTypeError("Import-map config resolve cannot be an accessor"); + } + const resolve = resolveDescriptor?.value; + if (resolve !== undefined && (resolve === null || typeof resolve !== "object")) { + throw new IntrinsicTypeError("Import-map config resolve must be an object"); + } + const importMapDescriptor = resolve + ? ObjectGetOwnPropertyDescriptor(resolve, "importMap") + : undefined; + if (importMapDescriptor && !hasOwn(importMapDescriptor, "value")) { + throw new IntrinsicTypeError("Import-map config resolve.importMap cannot be an accessor"); } + const importMap = snapshotEmbeddedImportMap(importMapDescriptor?.value); + // The loader only consumes resolve.importMap. Keeping the request snapshot + // minimal avoids invoking unrelated config getters or retaining mutable + // tenant-controlled configuration behind a cache entry. + const exactConfig = ObjectFreeze({ + resolve: ObjectFreeze({ + importMap, + }), + }) as VeryfrontConfig; + return ObjectFreeze({ contentSourceId, config: exactConfig, projectDir }); } -export function clearImportMapCache(cacheKey?: string): void { - if (cacheKey) { - importMapCache.delete(cacheKey); - return; +function buildVariantCanonicalIdentity( + context: PreloadImportMapContext, +): string { + const importMap = context.config?.resolve?.importMap; + let canonical = `${IMPORT_MAP_CACHE_IDENTITY_NAMESPACE}\0source:${ + JSONStringify(context.contentSourceId ?? null) + }\0projectDir:${JSONStringify(context.projectDir)}\0`; + if (!context.config) return `${canonical}ambient`; + + canonical += "validated"; + const imports = arraySort( + ObjectEntries(importMap?.imports ?? {}), + compareEntries, + ); + for (let index = 0; index < imports.length; index++) { + const entry = imports[index]!; + // JSON stringification is applied only to primitives. That keeps escaping + // canonical without exposing identity objects to inherited toJSON hooks. + canonical += `\0import:${JSONStringify(entry[0])}:${JSONStringify(entry[1])}`; + } + + const scopes = arraySort( + ObjectEntries(importMap?.scopes ?? {}), + compareEntries, + ); + for (let scopeIndex = 0; scopeIndex < scopes.length; scopeIndex++) { + const scopeEntry = scopes[scopeIndex]!; + canonical += `\0scope:${JSONStringify(scopeEntry[0])}`; + const mappings = arraySort(ObjectEntries(scopeEntry[1]), compareEntries); + for (let mappingIndex = 0; mappingIndex < mappings.length; mappingIndex++) { + const mapping = mappings[mappingIndex]!; + canonical += `\0mapping:${JSONStringify(mapping[0])}:${JSONStringify(mapping[1])}`; + } + } + return canonical; +} + +function readPositiveSafeInteger( + value: number | undefined, + fallback: number, + label: string, +): number { + const resolved = value ?? fallback; + if (!NumberIsSafeInteger(resolved) || resolved <= 0) { + throw new IntrinsicRangeError(`${label} must be a positive safe integer`); + } + return resolved; +} + +function readPositiveTimerMs(value: number | undefined, fallback: number): number { + const resolved = readPositiveSafeInteger(value, fallback, "loadTimeoutMs"); + if (resolved > MAX_TIMER_DELAY_MS) { + throw new IntrinsicRangeError( + `loadTimeoutMs must not exceed ${MAX_TIMER_DELAY_MS}`, + ); + } + return resolved; +} + +/** + * Bounded two-level import-map cache. + * + * Map insertion order is the LRU order at both levels. Expiry is lazy, avoiding + * a process-wide timer while still placing a hard ceiling on retained values. + */ +export class ImportMapPreloader { + private readonly projects = new IntrinsicMap(); + /** Underlying loader work remains accounted for even after explicit invalidation. */ + private readonly activeLoads = new IntrinsicSet>(); + private readonly activeIdentityBuilds = new IntrinsicSet>(); + /** Every underlying loader stays reserved here until it actually settles. */ + private readonly reservedLoadsByProject = new IntrinsicMap< + string, + Set> + >(); + private readonly orphanedLoadsByProject = new IntrinsicMap< + string, + Set> + >(); + private readonly capacityErrors = new IntrinsicWeakSet(); + private capacityChange = createCapacityChangeSignal(); + private globalGeneration = createGeneration(); + private readonly maxProjects: number; + private readonly maxVariantsPerProject: number; + private readonly maxConcurrentLoads: number; + private readonly ttlMs: number; + private readonly loadTimeoutMs: number; + private readonly now: () => number; + private readonly loader: typeof loadImportMap; + + constructor(options: ImportMapPreloaderOptions = {}) { + this.maxProjects = readPositiveSafeInteger( + options.maxProjects, + DEFAULT_MAX_IMPORT_MAP_PROJECTS, + "maxProjects", + ); + this.maxVariantsPerProject = readPositiveSafeInteger( + options.maxVariantsPerProject, + DEFAULT_MAX_IMPORT_MAP_VARIANTS_PER_PROJECT, + "maxVariantsPerProject", + ); + this.maxConcurrentLoads = MathMin( + NUMBER_MAX_SAFE_INTEGER, + this.maxProjects * this.maxVariantsPerProject, + ); + this.ttlMs = readPositiveSafeInteger( + options.ttlMs, + DEFAULT_IMPORT_MAP_TTL_MS, + "ttlMs", + ); + this.loadTimeoutMs = readPositiveTimerMs( + options.loadTimeoutMs, + DEFAULT_IMPORT_MAP_LOAD_TIMEOUT_MS, + ); + this.now = options.now ?? DateNow; + this.loader = options.loadImportMap ?? loadImportMap; + } + + private readNow(): number { + const now = this.now(); + if (!NumberIsFinite(now)) { + throw new IntrinsicRangeError("Import-map cache clock must be finite"); + } + return now; + } + + private touchProject(cacheKey: string, projectState: ProjectImportMapState): void { + mapDelete(this.projects, cacheKey); + mapSet(this.projects, cacheKey, projectState); + } + + private touchVariant( + projectCache: ProjectImportMapCache, + variantKey: string, + entry: CachedImportMap, + ): void { + mapDelete(projectCache, variantKey); + mapSet(projectCache, variantKey, entry); + } + + private deleteEntry( + cacheKey: string, + projectState: ProjectImportMapState, + variantKey: string, + entry: CachedImportMap, + removeEmptyProject = true, + ): void { + const projectCache = projectState.variants; + if (mapGet(projectCache, variantKey) !== entry) return; + mapDelete(projectCache, variantKey); + if ( + removeEmptyProject && + mapSize(projectCache) === 0 && + mapSize(projectState.identityBuilds) === 0 && + mapGet(this.projects, cacheKey) === projectState + ) { + mapDelete(this.projects, cacheKey); + } } - importMapCache.clear(); + private getEntry( + cacheKey: string, + variantKey: string, + now: number, + preserveProjectIfEmpty = false, + ): CachedImportMap | undefined { + const projectState = mapGet(this.projects, cacheKey); + const projectCache = projectState?.variants; + const entry = projectCache ? mapGet(projectCache, variantKey) : undefined; + if (!projectState || !projectCache || !entry) return undefined; + + if (entry.expiresAt !== null && entry.expiresAt <= now) { + this.deleteEntry( + cacheKey, + projectState, + variantKey, + entry, + !preserveProjectIfEmpty, + ); + return undefined; + } + + this.touchVariant(projectCache, variantKey, entry); + this.touchProject(cacheKey, projectState); + return entry; + } + + private getSingleVariantEntry( + cacheKey: string, + projectState: ProjectImportMapState, + now: number, + ): CachedImportMap | undefined { + const projectCache = projectState.variants; + let foundKey: string | undefined; + let foundEntry: CachedImportMap | undefined; + let foundEntries = 0; + + mapForEach(projectCache, (entry, variantKey) => { + if (entry.expiresAt !== null && entry.expiresAt <= now) { + mapDelete(projectCache, variantKey); + return; + } + foundEntries += 1; + if (foundEntries === 1) { + foundKey = variantKey; + foundEntry = entry; + } + }); + + if (foundEntries !== 1 || foundKey === undefined || foundEntry === undefined) { + this.removeEmptyProject(cacheKey, projectState); + return undefined; + } + + this.touchVariant(projectCache, foundKey, foundEntry); + this.touchProject(cacheKey, projectState); + return foundEntry; + } + + private capacityError(scope: "projects" | "variants" | "loads"): RangeError { + const error = new IntrinsicRangeError( + `Import-map preloader ${scope} capacity is occupied by in-flight loads; retry after a load settles`, + ); + weakSetAdd(this.capacityErrors, error); + return error; + } + + private isCapacityError(error: unknown): boolean { + return error !== null && typeof error === "object" && + weakSetHas(this.capacityErrors, error); + } + + private notifyCapacityChange(): void { + const signal = this.capacityChange; + this.capacityChange = createCapacityChangeSignal(); + signal.resolve(); + } + + private projectOrphanCount(cacheKey: string): number { + const orphanedLoads = mapGet(this.orphanedLoadsByProject, cacheKey); + return orphanedLoads ? setSize(orphanedLoads) : 0; + } + + private hasProjectLoadCapacity(cacheKey: string): boolean { + const reservedLoads = mapGet(this.reservedLoadsByProject, cacheKey); + return (reservedLoads ? setSize(reservedLoads) : 0) < + this.maxVariantsPerProject; + } + + private projectOccupancy(): number { + let occupied = mapSize(this.projects); + mapForEach(this.reservedLoadsByProject, (_loads, cacheKey) => { + if (!mapGet(this.projects, cacheKey)) occupied += 1; + }); + return occupied; + } + + private hasProjectOccupancy(cacheKey: string): boolean { + return mapGet(this.projects, cacheKey) !== undefined || + mapGet(this.reservedLoadsByProject, cacheKey) !== undefined; + } + + private waitForActiveWork( + capacityChange: Promise, + timeoutMs: number, + ): Promise { + const hasActiveWork = setSize(this.activeLoads) + setSize(this.activeIdentityBuilds) + + mapSize(this.reservedLoadsByProject) > + 0; + // Work can settle between the capacity check and this snapshot. Retry the + // admission loop immediately instead of surfacing a stale capacity error. + if (!hasActiveWork) return resolvedPromise(); + let timeoutId: ReturnType | undefined; + const timeout = new IntrinsicPromise((_, reject) => { + timeoutId = SetTimeout(() => { + reject(new IntrinsicRangeError("Import-map preloader capacity wait timed out")); + }, timeoutMs); + }); + return promiseThen( + raceTwo(capacityChange, timeout), + () => { + if (timeoutId !== undefined) ClearTimeout(timeoutId); + }, + (error) => { + if (timeoutId !== undefined) ClearTimeout(timeoutId); + throw error; + }, + ); + } + + private makeProjectRoom(now: number, requestedCacheKey: string): void { + mapForEach(this.projects, (projectState, cacheKey) => { + const projectCache = projectState.variants; + mapForEach(projectCache, (entry, variantKey) => { + if (entry.expiresAt !== null && entry.expiresAt <= now) { + mapDelete(projectCache, variantKey); + } + }); + if ( + mapSize(projectCache) === 0 && + mapSize(projectState.identityBuilds) === 0 && + mapGet(this.reservedLoadsByProject, cacheKey) === undefined + ) { + mapDelete(this.projects, cacheKey); + } + }); + + if (this.hasProjectOccupancy(requestedCacheKey)) return; + + while (this.projectOccupancy() >= this.maxProjects) { + let oldestSettledProject: string | undefined; + mapForEach(this.projects, (projectState, cacheKey) => { + if (oldestSettledProject !== undefined) return; + if (mapSize(projectState.identityBuilds) > 0) return; + if (mapGet(this.reservedLoadsByProject, cacheKey)) return; + let hasInFlightEntry = false; + mapForEach(projectState.variants, (entry) => { + if (entry.expiresAt === null) { + hasInFlightEntry = true; + } + }); + if (!hasInFlightEntry) { + oldestSettledProject = cacheKey; + } + }); + if (oldestSettledProject === undefined) throw this.capacityError("projects"); + mapDelete(this.projects, oldestSettledProject); + } + } + + private makeVariantRoom(projectCache: ProjectImportMapCache, now: number): void { + mapForEach(projectCache, (entry, variantKey) => { + if (entry.expiresAt !== null && entry.expiresAt <= now) { + mapDelete(projectCache, variantKey); + } + }); + + while (mapSize(projectCache) >= this.maxVariantsPerProject) { + let oldestSettledVariant: string | undefined; + mapForEach(projectCache, (entry, variantKey) => { + if (oldestSettledVariant !== undefined) return; + if (entry.expiresAt !== null) { + oldestSettledVariant = variantKey; + } + }); + if (oldestSettledVariant === undefined) throw this.capacityError("variants"); + mapDelete(projectCache, oldestSettledVariant); + } + } + + private trackOrphanedLoad( + cacheKey: string, + promise: Promise, + ): void { + let orphanedLoads = mapGet(this.orphanedLoadsByProject, cacheKey); + if (!orphanedLoads) { + orphanedLoads = new IntrinsicSet>(); + mapSet(this.orphanedLoadsByProject, cacheKey, orphanedLoads); + } + setAdd(orphanedLoads, promise); + void promiseThen( + promise, + () => { + this.releaseOrphanedLoad(cacheKey, promise); + }, + () => { + this.releaseOrphanedLoad(cacheKey, promise); + }, + ); + } + + private releaseOrphanedLoad( + cacheKey: string, + promise: Promise, + ): void { + const orphanedLoads = mapGet(this.orphanedLoadsByProject, cacheKey); + if (!orphanedLoads) return; + const removed = setDelete(orphanedLoads, promise); + if (setSize(orphanedLoads) === 0) { + mapDelete(this.orphanedLoadsByProject, cacheKey); + } + if (removed) this.notifyCapacityChange(); + } + + private reportTimedOutLoad(cacheKey: string): void { + // Hashing is asynchronous. Snapshot the transition counters first so the + // diagnostic describes this timeout rather than later concurrent changes. + const orphanedLoadsForProject = this.projectOrphanCount(cacheKey); + const orphanedProjects = mapSize(this.orphanedLoadsByProject); + const activeLoads = setSize(this.activeLoads); + void promiseThen( + computeHash(cacheKey), + (cacheKeyHash) => { + logger.warn("Import-map load timed out with underlying work still active", { + cacheKeyHash, + orphanedLoadsForProject, + orphanedProjects, + activeLoads, + }); + }, + () => { + logger.warn("Import-map load timed out with underlying work still active", { + orphanedLoadsForProject, + orphanedProjects, + activeLoads, + }); + }, + ); + } + + private trackActiveLoad(promise: Promise): void { + setAdd(this.activeLoads, promise); + promiseThen( + promise, + () => { + if (setDelete(this.activeLoads, promise)) this.notifyCapacityChange(); + }, + () => { + if (setDelete(this.activeLoads, promise)) this.notifyCapacityChange(); + }, + ); + } + + private reserveUnderlyingLoad( + cacheKey: string, + promise: Promise, + ): void { + let reservedLoads = mapGet(this.reservedLoadsByProject, cacheKey); + if (!reservedLoads) { + reservedLoads = new IntrinsicSet>(); + mapSet(this.reservedLoadsByProject, cacheKey, reservedLoads); + } + setAdd(reservedLoads, promise); + const release = (): void => { + const current = mapGet(this.reservedLoadsByProject, cacheKey); + if (!current || !setDelete(current, promise)) return; + if (setSize(current) === 0) { + mapDelete(this.reservedLoadsByProject, cacheKey); + } + this.notifyCapacityChange(); + }; + promiseThen(promise, release, release); + } + + private hasActiveWorkCapacity(): boolean { + return setSize(this.activeLoads) + setSize(this.activeIdentityBuilds) < + this.maxConcurrentLoads; + } + + private releaseIdentityBuild( + projectState: ProjectImportMapState, + canonicalIdentity: string, + promise: Promise, + ): void { + if (mapGet(projectState.identityBuilds, canonicalIdentity) === promise) { + mapDelete(projectState.identityBuilds, canonicalIdentity); + } + } + + private getOrCreateIdentityBuild( + projectState: ProjectImportMapState, + canonicalIdentity: string, + ): Promise { + const existing = mapGet(projectState.identityBuilds, canonicalIdentity); + if (existing) return existing; + if (mapSize(projectState.identityBuilds) >= this.maxVariantsPerProject) { + throw this.capacityError("variants"); + } + if (!this.hasActiveWorkCapacity()) throw this.capacityError("loads"); + + const promise = computeHash(canonicalIdentity); + mapSet(projectState.identityBuilds, canonicalIdentity, promise); + setAdd(this.activeIdentityBuilds, promise); + // Hash settlement frees global hashing capacity immediately. Keep the + // resolved per-project identity until its load settles, though, so a later + // request can reach and join that in-flight entry even when load capacity + // is otherwise full. + const releaseActive = (): void => { + if (setDelete(this.activeIdentityBuilds, promise)) this.notifyCapacityChange(); + }; + promiseThen( + promise, + releaseActive, + () => { + releaseActive(); + this.releaseIdentityBuild(projectState, canonicalIdentity, promise); + }, + ); + return promise; + } + + private isCurrentGeneration( + cacheKey: string, + projectState: ProjectImportMapState, + globalGeneration: object, + projectGeneration: object, + ): boolean { + return this.globalGeneration === globalGeneration && + projectState.generation === projectGeneration && + mapGet(this.projects, cacheKey) === projectState; + } + + private removeEmptyProject( + cacheKey: string, + projectState: ProjectImportMapState, + ): void { + if ( + mapSize(projectState.identityBuilds) === 0 && + mapSize(projectState.variants) === 0 && + mapGet(this.projects, cacheKey) === projectState + ) { + mapDelete(this.projects, cacheKey); + } + } + + private startTrackedLoad( + cacheKey: string, + projectDir: string, + adapter: RuntimeAdapter, + config: VeryfrontConfig | undefined, + ): Promise { + if (!this.hasProjectLoadCapacity(cacheKey)) { + throw this.capacityError("loads"); + } + if ( + !this.hasProjectOccupancy(cacheKey) && + this.projectOccupancy() >= this.maxProjects + ) { + throw this.capacityError("projects"); + } + if (!this.hasActiveWorkCapacity()) { + throw this.capacityError("loads"); + } + const loaderPromise = promiseThen( + resolvedPromise(), + () => this.loader(projectDir, adapter, config), + ); + // Reserve the project and its per-project load budget before the loader can + // start. A caller timeout can release global admission, but the underlying + // work retains this reservation until it actually settles. + this.reserveUnderlyingLoad(cacheKey, loaderPromise); + this.trackActiveLoad(loaderPromise); + let timeoutId: ReturnType | undefined; + const timeoutPromise = new IntrinsicPromise((_, reject) => { + timeoutId = SetTimeout(() => { + if (setDelete(this.activeLoads, loaderPromise)) { + this.trackOrphanedLoad(cacheKey, loaderPromise); + this.notifyCapacityChange(); + this.reportTimedOutLoad(cacheKey); + } + reject(new IntrinsicRangeError("Import-map preloader load timed out")); + }, this.loadTimeoutMs); + }); + const boundedLoaderPromise = promiseThen( + raceTwo(loaderPromise, timeoutPromise), + (value) => { + if (timeoutId !== undefined) ClearTimeout(timeoutId); + return value; + }, + (error) => { + if (timeoutId !== undefined) ClearTimeout(timeoutId); + throw error; + }, + ); + const promise = promiseThen( + boundedLoaderPromise, + (loadedImportMap) => snapshotImportMap(loadedImportMap), + ); + return promise; + } + + async preload( + projectDir: string, + adapter: RuntimeAdapter, + projectId?: string, + context?: PreloadImportMapContext, + ): Promise { + const capacityDeadline = monotonicNow() + this.loadTimeoutMs; + for (;;) { + const capacityChange = this.capacityChange.promise; + try { + return await this.preloadOnce(projectDir, adapter, projectId, context); + } catch (error) { + if (!this.isCapacityError(error)) throw error; + const remainingMs = capacityDeadline - monotonicNow(); + if (remainingMs <= 0) throw error; + await this.waitForActiveWork(capacityChange, remainingMs); + } + } + } + + private async preloadOnce( + projectDir: string, + adapter: RuntimeAdapter, + projectId?: string, + context?: PreloadImportMapContext, + ): Promise { + const exactContext = snapshotPreloadContext(projectDir, context); + const cacheKey = projectId ?? projectDir; + const canonicalIdentity = buildVariantCanonicalIdentity(exactContext); + const admissionNow = this.readNow(); + let projectState = mapGet(this.projects, cacheKey); + if (!projectState) { + this.makeProjectRoom(admissionNow, cacheKey); + projectState = { + variants: new IntrinsicMap(), + generation: createGeneration(), + identityBuilds: new IntrinsicMap(), + }; + mapSet(this.projects, cacheKey, projectState); + } else { + this.touchProject(cacheKey, projectState); + } + + // Joining the exact in-flight identity remains allowed at the per-project + // ceiling. A different identity cannot start, so reject it before hashing + // creates a settle-and-retry notification loop while an orphan is active. + if ( + mapGet(projectState.identityBuilds, canonicalIdentity) === undefined && + !this.hasProjectLoadCapacity(cacheKey) + ) { + this.removeEmptyProject(cacheKey, projectState); + throw this.capacityError("loads"); + } + + const globalGeneration = this.globalGeneration; + const projectGeneration = projectState.generation; + + let identityBuild: Promise | undefined; + let variantKey: string; + try { + identityBuild = this.getOrCreateIdentityBuild( + projectState, + canonicalIdentity, + ); + variantKey = await identityBuild; + } catch (error) { + if (identityBuild) { + this.releaseIdentityBuild( + projectState, + canonicalIdentity, + identityBuild, + ); + } + this.removeEmptyProject(cacheKey, projectState); + throw error; + } + const releaseIdentity = (): void => { + this.releaseIdentityBuild( + projectState, + canonicalIdentity, + identityBuild, + ); + }; + + // Explicit invalidation during asynchronous identity construction must not + // let pre-clear work enter the post-clear cache generation. The caller can + // still finish against its immutable request snapshot, but only as bounded, + // actively-accounted work. + if ( + !this.isCurrentGeneration( + cacheKey, + projectState, + globalGeneration, + projectGeneration, + ) + ) { + releaseIdentity(); + return this.startTrackedLoad( + cacheKey, + projectDir, + adapter, + exactContext.config, + ); + } + + let now: number; + try { + now = this.readNow(); + } catch (error) { + releaseIdentity(); + this.removeEmptyProject(cacheKey, projectState); + throw error; + } + // The injected clock is application code and can invalidate synchronously. + // Recheck after it runs so publication remains generation-atomic. + if ( + !this.isCurrentGeneration( + cacheKey, + projectState, + globalGeneration, + projectGeneration, + ) + ) { + releaseIdentity(); + return this.startTrackedLoad( + cacheKey, + projectDir, + adapter, + exactContext.config, + ); + } + + try { + // Direct expiry refresh must retain the authoritative project state. + // Removing the empty bucket here would publish the replacement into a + // detached Map that later callers cannot observe. + const cached = this.getEntry(cacheKey, variantKey, now, true); + if (cached) { + if (cached.expiresAt !== null) { + releaseIdentity(); + } + return cached.promise; + } + + const projectCache = projectState.variants; + this.makeVariantRoom(projectCache, now); + + const promise = this.startTrackedLoad( + cacheKey, + projectDir, + adapter, + exactContext.config, + ); + const entry: CachedImportMap = { promise, expiresAt: null }; + mapSet(projectCache, variantKey, entry); + + promiseThen( + promise, + () => { + releaseIdentity(); + if ( + mapGet(this.projects, cacheKey) !== projectState || + mapGet(projectCache, variantKey) !== entry + ) { + return; + } + let settledAt: number; + try { + settledAt = this.now(); + } catch (_) { + this.deleteEntry(cacheKey, projectState, variantKey, entry); + this.notifyCapacityChange(); + return; + } + if (!NumberIsFinite(settledAt)) { + this.deleteEntry(cacheKey, projectState, variantKey, entry); + this.notifyCapacityChange(); + return; + } + entry.expiresAt = MathMin( + NUMBER_MAX_SAFE_INTEGER, + settledAt + this.ttlMs, + ); + this.notifyCapacityChange(); + }, + () => { + releaseIdentity(); + this.deleteEntry(cacheKey, projectState, variantKey, entry); + this.notifyCapacityChange(); + }, + ); + + return promise; + } catch (error) { + releaseIdentity(); + this.removeEmptyProject(cacheKey, projectState); + throw error; + } + } + + async getCached( + cacheKey: string, + context?: PreloadImportMapContext, + ): Promise { + const projectDirDescriptor = context && typeof context === "object" + ? ObjectGetOwnPropertyDescriptor(context, "projectDir") + : undefined; + if (projectDirDescriptor && !hasOwn(projectDirDescriptor, "value")) { + throw new IntrinsicTypeError("Import-map projectDir cannot be an accessor"); + } + const contextProjectDir = projectDirDescriptor?.value; + if (contextProjectDir !== undefined && typeof contextProjectDir !== "string") { + throw new IntrinsicTypeError("Import-map projectDir must be a string"); + } + const projectState = mapGet(this.projects, cacheKey); + if (!projectState) return undefined; + const globalGeneration = this.globalGeneration; + const projectGeneration = projectState.generation; + let variantKey: string; + + // Before variants existed, callers could retrieve a projectId-keyed entry + // without also retaining its project directory. Preserve that contract + // only while the lookup is unambiguous. + if (context === undefined && mapSize(projectState.variants) === 1) { + let onlyVariantKey: string | undefined; + mapForEach(projectState.variants, (_entry, key) => { + onlyVariantKey = key; + }); + if (onlyVariantKey === undefined) return undefined; + variantKey = onlyVariantKey; + } else { + const exactContext = snapshotPreloadContext( + contextProjectDir ?? cacheKey, + context, + ); + const canonicalIdentity = buildVariantCanonicalIdentity(exactContext); + try { + variantKey = await computeHash(canonicalIdentity); + } catch (error) { + this.removeEmptyProject(cacheKey, projectState); + throw error; + } + } + if ( + !this.isCurrentGeneration( + cacheKey, + projectState, + globalGeneration, + projectGeneration, + ) + ) { + return undefined; + } + let entry: CachedImportMap | undefined; + try { + const now = this.readNow(); + entry = this.getEntry(cacheKey, variantKey, now) ?? + (context === undefined + ? this.getSingleVariantEntry(cacheKey, projectState, now) + : undefined); + } catch (error) { + this.removeEmptyProject(cacheKey, projectState); + throw error; + } + if (!entry) { + this.removeEmptyProject(cacheKey, projectState); + return undefined; + } + + try { + return await entry.promise; + } catch (_) { + /* expected: the rejection handler removes failed loads */ + return undefined; + } + } + + clear(cacheKey?: string): void { + if (cacheKey !== undefined) { + const projectState = mapGet(this.projects, cacheKey); + if (projectState) projectState.generation = createGeneration(); + mapDelete(this.projects, cacheKey); + return; + } + this.globalGeneration = createGeneration(); + mapClear(this.projects); + } +} + +const defaultImportMapPreloader = new ImportMapPreloader(); + +export function preloadImportMap( + projectDir: string, + adapter: RuntimeAdapter, + projectId?: string, + context?: PreloadImportMapContext, +): Promise { + return defaultImportMapPreloader.preload(projectDir, adapter, projectId, context); +} + +export function getCachedImportMap( + cacheKey: string, + context?: PreloadImportMapContext, +): Promise { + return defaultImportMapPreloader.getCached(cacheKey, context); +} + +export function clearImportMapCache(cacheKey?: string): void { + defaultImportMapPreloader.clear(cacheKey); } diff --git a/src/platform/compat/path/basic-operations.ts b/src/platform/compat/path/basic-operations.ts index fa7cc23f3b..c6fa957bf1 100644 --- a/src/platform/compat/path/basic-operations.ts +++ b/src/platform/compat/path/basic-operations.ts @@ -10,13 +10,20 @@ import { } from "./portable.ts"; import { getNativePathImplementation } from "./runtime.ts"; +const ArrayPrototypeEvery = Array.prototype.every; +const ArrayPrototypeSome = Array.prototype.some; +const ReflectApply = Reflect.apply; + function usesWindowsFlavor(paths: readonly string[]): boolean { - return runtimeUsesWindowsPaths() || paths.some(hasWindowsLikePath); + return runtimeUsesWindowsPaths() || + ReflectApply(ArrayPrototypeSome, paths, [hasWindowsLikePath]) as boolean; } /** Join and normalize path segments using their detected path flavor. */ export function join(...paths: string[]): string { - if (paths.every((path) => path.length === 0)) return "/"; + if ( + ReflectApply(ArrayPrototypeEvery, paths, [(path: string) => path.length === 0]) as boolean + ) return "/"; const windows = usesWindowsFlavor(paths); const pathApi = getNativePathImplementation(windows); const joined = pathApi diff --git a/src/platform/compat/path/portable.ts b/src/platform/compat/path/portable.ts index 2a1bcb4001..08179639c8 100644 --- a/src/platform/compat/path/portable.ts +++ b/src/platform/compat/path/portable.ts @@ -1,4 +1,11 @@ import type { PathObject } from "./types.ts"; +import { + primordialArrayAt as arrayAt, + primordialArrayFilter as arrayFilter, + primordialArrayJoin as arrayJoin, + primordialArrayPop as arrayPop, + primordialArrayPush as arrayPush, +} from "../primordials/array.ts"; interface RootInfo { absolute: boolean; @@ -78,15 +85,15 @@ function normalizeTail(rest: string, absolute: boolean): string[] { if (segment === "" || segment === ".") continue; if (segment !== "..") { - normalized.push(segment); + arrayPush(normalized, segment); continue; } - const previous = normalized.at(-1); + const previous = arrayAt(normalized, -1); if (previous !== undefined && previous !== "..") { - normalized.pop(); + arrayPop(normalized); } else if (!absolute) { - normalized.push(".."); + arrayPush(normalized, ".."); } } @@ -155,7 +162,7 @@ export function portableNormalize(path: string, windows: boolean): string { if (path === "") return "."; const root = analyzeRoot(path, windows); - const tail = normalizeTail(root.rest, root.absolute).join("/"); + const tail = arrayJoin(normalizeTail(root.rest, root.absolute), "/"); let result = appendRoot(root, tail); const hadTrailingSeparator = /[\\/]$/.test(path); @@ -172,9 +179,9 @@ export function portableNormalize(path: string, windows: boolean): string { } export function portableJoin(paths: readonly string[], windows: boolean): string { - const nonempty = paths.filter((path) => path.length > 0); + const nonempty = arrayFilter(paths, (path) => path.length > 0); if (nonempty.length === 0) return "/"; - return portableNormalize(nonempty.join("/"), windows); + return portableNormalize(arrayJoin(nonempty, "/"), windows); } export function portableDirname(path: string, windows: boolean): string { diff --git a/src/platform/compat/primordials/array.test.ts b/src/platform/compat/primordials/array.test.ts new file mode 100644 index 0000000000..2d43664b35 --- /dev/null +++ b/src/platform/compat/primordials/array.test.ts @@ -0,0 +1,69 @@ +import { assertEquals } from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; +import { + primordialArrayAt, + primordialArrayFilter, + primordialArrayJoin, + primordialArrayMap, + primordialArrayPop, + primordialArrayPush, + primordialArraySort, +} from "./array.ts"; + +describe("platform/compat/primordials/array", () => { + it("uses module-load-time captures after array prototypes are replaced", () => { + const originals = { + at: Array.prototype.at, + filter: Array.prototype.filter, + join: Array.prototype.join, + map: Array.prototype.map, + pop: Array.prototype.pop, + push: Array.prototype.push, + sort: Array.prototype.sort, + }; + const poisoned = () => { + throw new Error("poisoned array primordial"); + }; + + let first: number | undefined; + let filtered: number[] | undefined; + let joined: string | undefined; + let mapped: number[] | undefined; + let popped: number | undefined; + let sorted: number[] | undefined; + const values = [3, 1, 2]; + + try { + Array.prototype.at = poisoned; + Array.prototype.filter = poisoned; + Array.prototype.join = poisoned; + Array.prototype.map = poisoned; + Array.prototype.pop = poisoned; + Array.prototype.push = poisoned; + Array.prototype.sort = poisoned; + + first = primordialArrayAt(values, 0); + filtered = primordialArrayFilter(values, (value) => value > 1); + joined = primordialArrayJoin(values, ":"); + mapped = primordialArrayMap(values, (value) => value * 2); + primordialArrayPush(values, 4); + popped = primordialArrayPop(values); + sorted = primordialArraySort(values, (left, right) => left - right); + } finally { + Array.prototype.at = originals.at; + Array.prototype.filter = originals.filter; + Array.prototype.join = originals.join; + Array.prototype.map = originals.map; + Array.prototype.pop = originals.pop; + Array.prototype.push = originals.push; + Array.prototype.sort = originals.sort; + } + + assertEquals(first, 3); + assertEquals(filtered, [3, 2]); + assertEquals(joined, "3:1:2"); + assertEquals(mapped, [6, 2, 4]); + assertEquals(popped, 4); + assertEquals(sorted, [1, 2, 3]); + }); +}); diff --git a/src/platform/compat/primordials/array.ts b/src/platform/compat/primordials/array.ts new file mode 100644 index 0000000000..4ce6116853 --- /dev/null +++ b/src/platform/compat/primordials/array.ts @@ -0,0 +1,55 @@ +// Capture shared array intrinsics once, before project code can replace mutable +// prototype methods in a long-lived runtime. Keep this module dependency-free so +// low-level compatibility code and higher framework layers can use the same +// trusted operations without introducing an import cycle. +const ArrayPrototypeAt = Array.prototype.at; +const ArrayPrototypeFilter = Array.prototype.filter; +const ArrayPrototypeJoin = Array.prototype.join; +const ArrayPrototypeMap = Array.prototype.map; +const ArrayPrototypePop = Array.prototype.pop; +const ArrayPrototypePush = Array.prototype.push; +const ArrayPrototypeSort = Array.prototype.sort; +const ReflectApply = Reflect.apply; + +export function primordialArrayAt( + values: readonly T[], + index: number, +): T | undefined { + return ReflectApply(ArrayPrototypeAt, values, [index]) as T | undefined; +} + +export function primordialArrayFilter( + values: readonly T[], + predicate: (value: T, index: number, array: readonly T[]) => unknown, +): T[] { + return ReflectApply(ArrayPrototypeFilter, values, [predicate]) as T[]; +} + +export function primordialArrayJoin( + values: readonly unknown[], + separator: string, +): string { + return ReflectApply(ArrayPrototypeJoin, values, [separator]) as string; +} + +export function primordialArrayMap( + values: readonly T[], + callback: (value: T, index: number, array: readonly T[]) => U, +): U[] { + return ReflectApply(ArrayPrototypeMap, values, [callback]) as U[]; +} + +export function primordialArrayPop(values: T[]): T | undefined { + return ReflectApply(ArrayPrototypePop, values, []) as T | undefined; +} + +export function primordialArrayPush(values: T[], value: T): void { + ReflectApply(ArrayPrototypePush, values, [value]); +} + +export function primordialArraySort( + values: T[], + compare: (left: T, right: T) => number, +): T[] { + return ReflectApply(ArrayPrototypeSort, values, [compare]) as T[]; +} diff --git a/src/react/components/chat/chat/hooks/use-upload.test.tsx b/src/react/components/chat/chat/hooks/use-upload.test.tsx index 4a5c8c87ba..9aeba06c80 100644 --- a/src/react/components/chat/chat/hooks/use-upload.test.tsx +++ b/src/react/components/chat/chat/hooks/use-upload.test.tsx @@ -9,6 +9,7 @@ import { assertThrows, } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; +import { waitFor } from "#veryfront/testing/deno-compat.ts"; import { createUploadId, parseChatUploadResponse, @@ -650,7 +651,10 @@ describe("useUpload", () => { , ); }); - await new Promise((resolve) => setTimeout(resolve, 0)); + await waitFor(() => attemptedSuspendedRender, { + interval: 1, + message: "Concurrent upload render did not start", + }); assertEquals(attemptedSuspendedRender, true); assertEquals(request.aborted, false); diff --git a/src/react/components/chat/chat/hooks/use-uploads-registry.test.tsx b/src/react/components/chat/chat/hooks/use-uploads-registry.test.tsx index 18d1e18af4..db55bbb9d5 100644 --- a/src/react/components/chat/chat/hooks/use-uploads-registry.test.tsx +++ b/src/react/components/chat/chat/hooks/use-uploads-registry.test.tsx @@ -5,6 +5,7 @@ import { JSDOM } from "npm:jsdom@28.0.0"; import { unmountReactRoot } from "#veryfront/react/react-root.test-helpers.ts"; import { assert, assertEquals } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; +import { waitFor } from "#veryfront/testing/deno-compat.ts"; import { useAttachments, useUploadsRegistry, @@ -1055,7 +1056,10 @@ describe("react/components/chat/hooks/useUploadsRegistry", () => { , ); }); - await new Promise((resolve) => setTimeout(resolve, 0)); + await waitFor(() => attemptedSuspendedRender, { + interval: 1, + message: "Concurrent endpoint render did not start", + }); assertEquals(attemptedSuspendedRender, true); assertEquals(pending[0]?.signal?.aborted, false); assertEquals(pending.length, 1, "an uncommitted scope must not start a refresh"); diff --git a/src/rendering/layouts/layout-applicator.ts b/src/rendering/layouts/layout-applicator.ts index 2b818274f1..387484c410 100644 --- a/src/rendering/layouts/layout-applicator.ts +++ b/src/rendering/layouts/layout-applicator.ts @@ -288,6 +288,7 @@ export class LayoutApplicator { this.dependencyPinningDependencies, this.dependencyPinningSource, this.requestUrl?.origin, + this.config, ); } diff --git a/src/rendering/layouts/utils/applicator.ts b/src/rendering/layouts/utils/applicator.ts index c0687bb508..e271f06778 100644 --- a/src/rendering/layouts/utils/applicator.ts +++ b/src/rendering/layouts/utils/applicator.ts @@ -1,6 +1,7 @@ import { rendererLogger } from "#veryfront/utils"; import * as BundledReact from "react"; import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; +import type { VeryfrontConfig } from "#veryfront/config"; import type { LayoutItem, MdxBundle, MDXComponents } from "#veryfront/types"; import type { ImportMapConfig } from "#veryfront/modules/import-map/types.ts"; import { SpanNames } from "#veryfront/observability"; @@ -33,6 +34,7 @@ export function applyLayoutsESM( dependencyPinningDependencies?: Readonly>, dependencyPinningSource?: DependencyPinningSourceInput, moduleServerOrigin?: string, + config?: VeryfrontConfig, ): Promise { return withSpan( SpanNames.LAYOUT_APPLY_LAYOUTS_ESM, @@ -83,6 +85,7 @@ export function applyLayoutsESM( dependencyPinningDependencies, dependencyPinningSource, moduleServerOrigin, + config, ), spanAttrs, ); @@ -145,6 +148,7 @@ export function applyLayoutsESM( dependencyPinningDependencies, dependencyPinningSource, moduleServerOrigin, + config, ), { "layout.kind": "mdx", "layout.type": "named" }, ); diff --git a/src/rendering/layouts/utils/component-loader.test.ts b/src/rendering/layouts/utils/component-loader.test.ts index 213ccfbca8..5411e26b94 100644 --- a/src/rendering/layouts/utils/component-loader.test.ts +++ b/src/rendering/layouts/utils/component-loader.test.ts @@ -14,6 +14,11 @@ import { mdxRenderer } from "#veryfront/transforms/mdx/index.ts"; import type { MdxBundle } from "#veryfront/types"; import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; import { hashString } from "#veryfront/cache/hash.ts"; +import { validateVeryfrontConfig } from "#veryfront/config"; +import { + clearImportMapCache, + getCachedImportMap, +} from "#veryfront/modules/import-map/preloader.ts"; function cacheKeyForDependencies( dependencies: Readonly>, @@ -519,6 +524,75 @@ describe("rendering/layouts/utils/component-loader", () => { } }); + it("preloads the MDX import map under the exact request context", async () => { + clearImportMapCache(); + const originalLoadModuleESM = mdxRenderer.loadModuleESM; + const mutableRenderer = mdxRenderer as unknown as { + loadModuleESM: typeof mdxRenderer.loadModuleESM; + }; + mutableRenderer.loadModuleESM = + (() => Promise.resolve({ default: () => null })) as typeof mdxRenderer.loadModuleESM; + + const adapter = { + fs: { + readFile: () => { + const error = new Error("not found") as Error & { code: string }; + error.code = "ENOENT"; + throw error; + }, + exists: () => false, + }, + env: { get: () => undefined }, + } as unknown as RuntimeAdapter; + const config = validateVeryfrontConfig({ + resolve: { + importMap: { + imports: { "context-package": "https://example.com/context-package.ts" }, + }, + }, + }); + + try { + await loadMDXLayout( + { compiledCode: "export default function Layout() { return null; }" } as MdxBundle, + "/context-project", + adapter, + "context-project-id", + "project-slug", + "release-1", + undefined, + "19.1.0", + SNAPSHOT_A_PIN_KEY, + SNAPSHOT_A_DEPENDENCIES, + undefined, + undefined, + config, + ); + + // The production call site must register the preloaded map under the + // exact release/config variant, not the ambient projectId-only variant. + const exactVariant = await getCachedImportMap("context-project-id", { + projectDir: "/context-project", + contentSourceId: "release-1", + config, + }); + assertEquals( + exactVariant?.imports?.["context-package"], + "https://example.com/context-package.ts", + ); + + const otherContentSource = await getCachedImportMap("context-project-id", { + projectDir: "/context-project", + contentSourceId: "release-2", + config, + }); + assertEquals(otherContentSource, undefined); + } finally { + mutableRenderer.loadModuleESM = originalLoadModuleESM; + clearImportMapCache(); + } + }); + it("uses the request snapshot in the TSX layout cache key", async () => { function CachedLayout() { return null; diff --git a/src/rendering/layouts/utils/component-loader.ts b/src/rendering/layouts/utils/component-loader.ts index 4f78b98fe8..9602bc1c5a 100644 --- a/src/rendering/layouts/utils/component-loader.ts +++ b/src/rendering/layouts/utils/component-loader.ts @@ -6,6 +6,7 @@ import { } from "#veryfront/utils"; import * as BundledReact from "react"; import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; +import type { VeryfrontConfig } from "#veryfront/config"; import type { LayoutItem, MdxBundle, MDXComponents, MDXModule } from "#veryfront/types"; import type { ImportMapConfig } from "#veryfront/modules/import-map/types.ts"; import { createError, toError } from "#veryfront/errors"; @@ -346,6 +347,7 @@ export function loadMDXLayout( dependencyPinningDependencies?: Readonly>, dependencyPinningSource?: DependencyPinningSourceInput, moduleServerOrigin?: string, + config?: VeryfrontConfig, ): Promise | undefined> { return withSpan( SpanNames.LAYOUT_LOAD_MDX, @@ -355,7 +357,11 @@ export function loadMDXLayout( hasPreloadedImportMap: !!preloadedImportMap, }); - const map = preloadedImportMap ?? (await preloadImportMap(projectDir, adapter, projectId)); + const map = preloadedImportMap ?? (await preloadImportMap(projectDir, adapter, projectId, { + projectDir, + contentSourceId, + config, + })); if (preloadedImportMap) { loadMdxLayoutLog.debug("Using preloaded import map", { projectSlug }); } @@ -408,6 +414,7 @@ export async function preloadMDXLayoutModule( dependencyPinningDependencies?: Readonly>, dependencyPinningSource?: DependencyPinningSourceInput, moduleServerOrigin?: string, + config?: VeryfrontConfig, ): Promise { await loadMDXLayout( bundle, @@ -422,6 +429,7 @@ export async function preloadMDXLayoutModule( dependencyPinningDependencies, dependencyPinningSource, moduleServerOrigin, + config, ); } @@ -508,6 +516,7 @@ export async function applyMDXLayout( dependencyPinningDependencies?: Readonly>, dependencyPinningSource?: DependencyPinningSourceInput, moduleServerOrigin?: string, + config?: VeryfrontConfig, ): Promise { const React = await getProjectReact(reactVersion); const LayoutFn = await loadMDXLayout( @@ -523,6 +532,7 @@ export async function applyMDXLayout( dependencyPinningDependencies, dependencyPinningSource, moduleServerOrigin, + config, ); if (!LayoutFn) { diff --git a/src/rendering/orchestrator/layout.test.ts b/src/rendering/orchestrator/layout.test.ts new file mode 100644 index 0000000000..f42eb824c8 --- /dev/null +++ b/src/rendering/orchestrator/layout.test.ts @@ -0,0 +1,106 @@ +import "#veryfront/schemas/_test-setup.ts"; +import { assertEquals } from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; +import { LayoutOrchestrator } from "./layout.ts"; +import { createLayoutComponentCache } from "../layouts/utils/component-loader.ts"; +import type { LayoutCollector, LayoutCompiler } from "../layouts/index.ts"; +import { mdxRenderer } from "#veryfront/transforms/mdx/index.ts"; +import { validateVeryfrontConfig } from "#veryfront/config"; +import { + clearImportMapCache, + getCachedImportMap, +} from "#veryfront/modules/import-map/preloader.ts"; +import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; +import type { LayoutItem, MdxBundle } from "#veryfront/types"; + +function createMissingFileAdapter(): RuntimeAdapter { + return { + fs: { + readFile: () => { + const error = new Error("not found") as Error & { code: string }; + error.code = "ENOENT"; + throw error; + }, + exists: () => false, + }, + env: { get: () => undefined }, + } as unknown as RuntimeAdapter; +} + +describe("rendering/orchestrator/layout", () => { + it("preloads the MDX import map under the exact request context", async () => { + clearImportMapCache(); + const originalLoadModuleESM = mdxRenderer.loadModuleESM; + const mutableRenderer = mdxRenderer as unknown as { + loadModuleESM: typeof mdxRenderer.loadModuleESM; + }; + mutableRenderer.loadModuleESM = + (() => Promise.resolve({ default: () => null })) as typeof mdxRenderer.loadModuleESM; + + const config = validateVeryfrontConfig({ + resolve: { + importMap: { + imports: { + "orchestrator-package": "https://example.com/orchestrator-package.ts", + }, + }, + }, + }); + const orchestrator = new LayoutOrchestrator({ + projectDir: "/orchestrator-project", + projectId: "orchestrator-project-id", + projectSlug: "orchestrator-slug", + contentSourceId: "release-1", + adapter: createMissingFileAdapter(), + config, + mode: "production", + layoutCollector: {} as LayoutCollector, + layoutCompiler: {} as LayoutCompiler, + layoutCache: createLayoutComponentCache(), + componentRegistry: {}, + }); + const mdxLayout: LayoutItem = { + kind: "mdx", + path: "/orchestrator-project/layout.mdx", + bundle: { + compiledCode: "export default function Layout() { return null; }", + } as MdxBundle, + }; + + try { + const summary = await orchestrator.preloadLayoutModules( + [mdxLayout], + undefined, + { react: "19.1.0" }, + ); + + assertEquals(summary.importMapSuccess, true); + assertEquals( + orchestrator.getPreloadedImportMap()?.imports?.["orchestrator-package"], + "https://example.com/orchestrator-package.ts", + ); + + // The orchestrator call site must register the preloaded map under the + // exact release/config variant, not the ambient projectId-only variant. + const exactVariant = await getCachedImportMap("orchestrator-project-id", { + projectDir: "/orchestrator-project", + contentSourceId: "release-1", + config, + }); + assertEquals( + exactVariant?.imports?.["orchestrator-package"], + "https://example.com/orchestrator-package.ts", + ); + + const otherContentSource = await getCachedImportMap("orchestrator-project-id", { + projectDir: "/orchestrator-project", + contentSourceId: "release-2", + config, + }); + assertEquals(otherContentSource, undefined); + } finally { + mutableRenderer.loadModuleESM = originalLoadModuleESM; + clearImportMapCache(); + } + }); +}); diff --git a/src/rendering/orchestrator/layout.ts b/src/rendering/orchestrator/layout.ts index 947bac03a3..ef669afb72 100644 --- a/src/rendering/orchestrator/layout.ts +++ b/src/rendering/orchestrator/layout.ts @@ -178,6 +178,11 @@ export class LayoutOrchestrator { this.config.projectDir, this.config.adapter, this.config.projectId, + { + projectDir: this.config.projectDir, + contentSourceId: this.config.contentSourceId, + config: this.config.config, + }, ); this._preloadedImportMap = importMap; return { type: "importMap" as const, success: true }; @@ -249,6 +254,7 @@ export class LayoutOrchestrator { dependencyPinningDependencies, dependencyPinningSource, moduleServerOrigin, + this.config.config, ); return { type: "mdx" as const, path: layout.path, success: true }; } catch (error) { diff --git a/src/server/services/rsc/endpoints/rsc-bundles.generated.ts b/src/server/services/rsc/endpoints/rsc-bundles.generated.ts index a1d3fd6f69..65bb090cbb 100644 --- a/src/server/services/rsc/endpoints/rsc-bundles.generated.ts +++ b/src/server/services/rsc/endpoints/rsc-bundles.generated.ts @@ -7,7 +7,7 @@ */ export const CLIENT_BOOT_BUNDLE: string = - 'var at=Object.defineProperty;var ct=(e,t,r)=>t in e?at(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var m=(e,t,r)=>ct(e,typeof t!="symbol"?t+"":t,r);var ut="3.2.3";function lt(e,t,r,n){let s=[];if(n?.external?.length&&s.push(`external=${n.external.join(",")}`),s.push(`target=${n?.target??"es2022"}`),n?.deps){let d=Object.entries(n.deps).map(([c,g])=>`${c}@${g}`).join(",");s.push(`deps=${d}`)}let i=t?`@${t}`:"",a=r??"",u=s.length?`?${s.join("&")}`:"";return`https://esm.sh/${e}${i}${a}${u}`}function _(e,t,r,n=!1){return lt(e,t,r,{external:n?["react"]:void 0,deps:{csstype:ut}})}var gt="19.2.4",O=gt;function Ee(e=O){return{react:_("react",e),"react-dom":_("react-dom",e,void 0,!0),"react-dom/client":_("react-dom",e,"/client",!0),"react-dom/server":_("react-dom",e,"/server",!0),"react/jsx-runtime":_("react",e,"/jsx-runtime",!0),"react/jsx-dev-runtime":_("react",e,"/jsx-dev-runtime",!0)}}function Re(e=O){return Ee(e).react}function he(e=O){return Ee(e)["react-dom/client"]}function ft(e){return e.replaceAll("+","-").replaceAll("/","_").replaceAll("=","")}function pt(e){if(typeof globalThis.btoa=="function")try{return globalThis.btoa(e)}catch{return yt(new TextEncoder().encode(e))}let t=globalThis.Buffer;if(t)return t.from(e,"utf8").toString("base64");throw new Error("Base64 encoding is not supported in this runtime")}function yt(e){let t=globalThis.Buffer;if(t)return t.from(e).toString("base64");if(typeof globalThis.btoa=="function"){let r="";for(let n of e)r+=String.fromCharCode(n);return globalThis.btoa(r)}throw new Error("Base64 encoding is not supported in this runtime")}function te(e){return ft(pt(e))}var Qo=Object.freeze({react:"","react-dom":"","react-dom/client":"","react-dom/server":"","react/jsx-runtime":"","react/jsx-dev-runtime":""});function U(e,t){if(!t)return!1;if(Object.prototype.hasOwnProperty.call(t,e))return!0;for(let r of Object.keys(t))if(r.endsWith("/")&&e.startsWith(r))return!0;return!1}function mt(e){try{return JSON.parse(e)?.imports??{}}catch(t){return console.warn("Failed to parse import map JSON; treating as empty",{errorName:t instanceof Error?t.name:typeof t,inputLength:e.length}),{}}}function re(e=document){let t=e.querySelector(\'script[type="importmap"]\');return t?.textContent?mt(t.textContent):{}}var ts=Object.freeze({IPV4:"127.0.0.1",IPV6:"::1",HOSTNAME:"localhost"});var Et=5e3,Rt=1e4,os=16*1024*1024,ht=5e3;var _t=100;var xt=Object.freeze([5,10,25,50,75,100,250,500,750,1e3,2500,5e3,7500,1e4]),ss=Object.freeze([1,5,10,25,50,100,250,500,1e3,2500,5e3,1e4]),is=Object.freeze({server:Object.freeze({port:3e3,hostname:"0.0.0.0"}),timeouts:Object.freeze({default:Et,api:3e4,ssr:Rt,hmr:3e4,sandbox:ht}),cache:Object.freeze({jit:Object.freeze({maxSize:_t,tempDirPrefix:"vf-bundle-"})}),metrics:Object.freeze({ssrBoundaries:xt})});var p="/_veryfront",ne={RSC:`${p}/rsc/`,FS:`${p}/fs/`,MODULES:`${p}/modules/`,PAGES:`${p}/pages/`,DATA:`${p}/data/`,LIB:`${p}/lib/`,CHUNKS:`${p}/chunks/`,CLIENT:`${p}/client/`},xe={HMR_RUNTIME:`${p}/hmr-runtime.js`,HMR:`${p}/hmr.js`,ERROR_OVERLAY:`${p}/error-overlay.js`,DEV_LOADER:`${p}/dev-loader.js`,CLIENT_LOG:`${p}/log`,CLIENT_JS:`${p}/client.js`,ROUTER_JS:`${p}/router.js`,PREFETCH_JS:`${p}/prefetch.js`,MANIFEST_JSON:`${p}/manifest.json`,APP_JS:`${p}/app.js`,RSC_CLIENT:`${p}/rsc/client.js`,RSC_MANIFEST:`${p}/rsc/manifest`,RSC_STREAM:`${p}/rsc/stream`,RSC_PAYLOAD:`${p}/rsc/payload`,RSC_RENDER:`${p}/rsc/render`,RSC_PAGE:`${p}/rsc/page`,RSC_MODULE:`${p}/rsc/module`,RSC_DOM:`${p}/rsc/dom.js`,LIB_CHAT_REACT:`${p}/lib/chat/react.js`,LIB_CHAT_COMPONENTS:`${p}/lib/chat/components.js`,LIB_CHAT_PRIMITIVES:`${p}/lib/chat/primitives.js`};var Tt={ROOT:".veryfront",CACHE:".veryfront/cache",KV:".veryfront/kv",LOGS:".veryfront/logs",TMP:".veryfront/tmp"},cs=Tt.CACHE;var us={HMR_RUNTIME:xe.HMR_RUNTIME,ERROR_OVERLAY:xe.ERROR_OVERLAY};var I=ne.RSC,Te=ne.FS;var N="rsc-root",k="x-veryfront-dependency-pins";var T=class{constructor(t,r){m(this,"prefix",t);m(this,"level",r)}log(t,r,n,...s){this.level>t||r?.(n,...s)}debug(t,...r){this.log(0,console.debug,`[${this.prefix}] DEBUG: ${t}`,...r)}info(t,...r){this.log(1,console.log,`[${this.prefix}] ${t}`,...r)}warn(t,...r){this.log(2,console.warn,`[${this.prefix}] WARN: ${t}`,...r)}error(t,...r){this.log(3,console.error,`[${this.prefix}] ERROR: ${t}`,...r)}};function St(){if(typeof window>"u")return 2;let e=globalThis;return e.__VERYFRONT_DEV__||e.__RSC_DEV__?e.__VERYFRONT_DEBUG__||e.__RSC_DEBUG__?0:1:2}var $=St(),l=new T("RSC",$),ps=new T("PREFETCH",$),ys=new T("HYDRATE",$),ms=new T("VERYFRONT",$);var Ct="veryfront-hydration-data";function oe(e){try{let t=[...e.querySelectorAll(`[id="${Ct}"]`)];if(t.length!==1)return null;let r=e.body;if(!r)return null;let n=t[0];return r.firstElementChild!==n&&n.parentElement!==r||n.tagName?.toLowerCase()!=="script"||n.getAttribute("type")?.trim().toLowerCase()!=="application/json"?null:n}catch{return null}}function S(e=document){try{let t=oe(e);return t?JSON.parse(t.textContent||"{}"):null}catch(t){return l.debug("hydration data parse failed",t),null}}function V(e,t){if(!t?.startsWith("on:"))return!1;try{let r=oe(e);if(!r)return!1;let n=JSON.parse(r.textContent||"{}");return n.dependencyPinningCacheKey=t,r.textContent=JSON.stringify(n),!0}catch(r){return l.debug("hydration dependency snapshot seed failed",r),!1}}function F(e){return e?.clientModuleStrategy?e.clientModuleStrategy:e?.dev?"fs":"rsc-module"}function At(e,t){if(!t)return e;let r=e.includes("?")?"&":"?";return`${e}${r}v=${encodeURIComponent(t)}`}function B(e,t){if(!t?.startsWith("on:"))return e;let r=e.indexOf("#"),n=r===-1?"":e.slice(r),s=r===-1?e:e.slice(0,r),i=s.indexOf("?"),a=i===-1?s:s.slice(0,i),u=new URLSearchParams(i===-1?"":s.slice(i+1));u.set("pins",t);let d=u.toString();return`${a}${d?`?${d}`:""}${n}`}function bt(e,t){return At(`${Te}${te(e)}.js`,t)}function Ot(e,t,r){let n=t?`&v=${encodeURIComponent(t)}`:"";return B(`${I}module?rel=${encodeURIComponent(e)}${n}`,r)}function D(e){let t=e?.dependencyPinningCacheKey;return t?.startsWith("on:")?{[k]:t}:{}}function It(e){return e.replace(/^\\/+_vf_modules\\//,"").replace(/^\\/+/,"").replace(/\\.js$/,"")}var Nt=/\\.(tsx|ts|jsx|mdx|js)$/;function Dt(e){let t=It(e),r=[e,t];return Nt.test(t)||r.push(`${t}.tsx`,`${t}.ts`,`${t}.jsx`,`${t}.mdx`,`${t}.js`),Array.from(new Set(r))}function wt(e,t){if(!e)return null;for(let r of Dt(t)){let n=e[r];if(n)return n}return null}function G(e){if(e.strategy==="fs"){let r=e.absPath??e.rel;return r?B(bt(r,e.version),e.dependencyPinningCacheKey):null}let t=wt(e.releaseAssetModules,e.rel);return t||Ot(e.rel,e.version,e.dependencyPinningCacheKey)}function j(e=document,t=O){let r=re(e);return{react:U("react",r)?"react":Re(t),reactDomClient:U("react-dom/client",r)?"react-dom/client":he(t)}}function Se(e=document){let t=re(e);return U("veryfront/router",t)?"veryfront/router":null}var Mt=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function Lt(e,...t){let r=Object.create(null),n=e.charAt(0).toUpperCase()+e.slice(1);for(let s of t)for(let[i,a]of Object.entries(s)){if(typeof a!="object"||a===null||!Object.hasOwn(a,"slug"))throw new Error(`${n} entry "${i}" must define a slug`);let u=a.slug;if(typeof u!="string")throw new Error(`${n} entry "${i}" must define a string slug`);if(u!==i)throw new Error(`${n} key "${i}" does not match entry slug "${u}"`);if(Object.hasOwn(r,i))throw new Error(`Duplicate ${e} slug "${i}"`);r[i]=a}return Object.freeze(r)}function Ce(...e){for(let t of e)for(let r of Object.values(t)){if(typeof r.slug!="string"||r.slug.length<3||r.slug.length>40||!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(r.slug))throw new TypeError(`Registered error slug must be 3-40 characters of lowercase kebab-case, got "${r.slug}"`);if(typeof r.category!="string"||!Mt.has(r.category))throw new TypeError(`Registered error has unknown category "${r.category}"`);if(!Number.isInteger(r.status)||r.status<400||r.status>=600)throw new RangeError(`Registered error status must be an integer from 400 through 599, got ${r.status}`);if(typeof r.title!="string"||r.title.trim().length===0)throw new TypeError("Registered error title must be a non-empty string");if(r.suggestion!==void 0&&(typeof r.suggestion!="string"||r.suggestion.trim().length===0))throw new TypeError("Registered error suggestion must be non-empty when provided")}return Lt("error registry",...e)}var z={reset:"\\x1B[0m",dim:"\\x1B[2m",gray:"\\x1B[90m",red:"\\x1B[31m",green:"\\x1B[32m",yellow:"\\x1B[33m",blue:"\\x1B[34m",magenta:"\\x1B[35m",cyan:"\\x1B[36m"},Is={debug:z.gray,info:z.green,warn:z.yellow,error:z.red};var y="[REDACTED]",E=Reflect.apply;var Ae=RegExp.prototype.exec,x=RegExp.prototype[Symbol.replace],Ds=String.prototype.charCodeAt,be=String.prototype.slice,Pt=String.prototype.toLowerCase,Ht=/[^a-z0-9]/g;function se(e){let t=E(Pt,e,[]);return E(x,Ht,[t,""])}function Y(e,t,r){return r===void 0?E(be,e,[t]):E(be,e,[t,r])}var vt=["password","passwd","pwd","passphrase","secret","clientsecret","token","apikey","accesskey","privatekey","credential","authorization","cookie","bearer","jwt","connectionstring","signature","sessionid","sid","otp","mfa","pin","salt","xsrf","csrf"],Ut=512,kt=128,w=new Map;function Ne(e){let t=e.length<=kt;if(t){let s=w.get(e);if(s!==void 0)return s}let r=se(e),n=vt.some(s=>r.includes(s));if(t){if(w.size>=Ut){let s=w.keys().next().value;s!==void 0&&w.delete(s)}w.set(e,n)}return n}var $t=["access_token","accesstoken","refresh_token","api_key","apikey","code","token","secret","client_secret","password","passwd","pwd","state","sig","signature","auth","x-amz-credential","x-amz-signature","x-amz-security-token","x-goog-credential","x-goog-signature"],Vt=new Set($t.map(se)),Ft=/(\\b[a-z][a-z0-9+.-]*:\\/\\/|\\/\\/)([^/?#\\s]+)@/gi,Bt=/(\\b[a-z][a-z0-9+.-]*:\\/\\/|\\/\\/)([a-z0-9._~!$&\'()*+,;=%-]+):([^/?#@\\r\\n \\t]+[ \\t][^/?#@\\r\\n]*)@/gi,Gt=3;function jt(e){return e===" "||e==="\t"||e===","||e===";"||e==="&"||e==="?"||e==="#"}function zt(e){if(!e)return!1;let t=e.charCodeAt(0);return t>=65&&t<=90||t>=97&&t<=122}function De(e){return zt(e)||e==="_"||e==="$"}function Yt(e){if(!e)return!1;let t=e.charCodeAt(0);return De(e)||t>=48&&t<=57||e==="."||e==="-"}function we(e,t){let r=t,n=e[r]===\'"\'||e[r]==="\'"?e[r++]:"";if(!De(e[r]))return!1;for(r++;Yt(e[r]);)r++;if(n){if(e[r]!==n)return!1;r++}for(;e[r]===" "||e[r]==="\t";)r++;return e[r]===":"||e[r]==="="}function Me(e){return e==="\\r"||e===`\n`||e==="}"||e==="]"||jt(e)}function Le(e,t){let r=t;for(;r=e.length||we(e,r)}function Kt(e,t){let r=t,n=!0;if(e.startsWith(y,t)){let g=t+y.length;if(Oe(e,g))return{end:g,replacement:y};r=g,n=!1}let s=n&&(e[r]===\'"\'||e[r]==="\'"||e[r]==="`")?e[r]:"",i=!1,a=()=>s?`${s}${y}${i?s:""}`:y,u=[],d="",c=-1;for(let g=r;g0&&(f==="}"||f==="]")){if(u.at(-1)!==f)return{end:e.length,replacement:a()};if(u.pop(),g++,u.length===0&&Oe(e,g))return{end:g,replacement:a()};continue}if(u.length>0||!Me(f)){g++;continue}let R=g;if(g=Le(e,g),g>=e.length||we(e,g))return{end:R,replacement:a()}}return{end:e.length,replacement:a()}}function Ie(e,t,r,n){let s=0,i="";for(let a=E(Ae,t,[e]);a;a=E(Ae,t,[e])){let u=a[r];if(!Ne(u))continue;let d=t.lastIndex,c=n===void 0?void 0:a[n],g=d+y.length;if((c==="?"||c==="&"||c===";")&&e.startsWith(y,d)&&e[g]==="#")continue;let f=Kt(e,d);i+=Y(e,s,a.index),i+=a[0],i+=f.replacement,s=f.end,t.lastIndex=f.end}return s===0?e:i+Y(e,s)}function Wt(e,t,r){let n=r.search(/[ \\t]/);if(n<0)return!1;let s=`${t}:${Y(r,0,n)}`,i=e==="//"?`https://${s}`:`${e}${s}`;try{let a=new URL(i);return a.username.length===0&&a.password.length===0}catch{return!1}}function Xt(e){let t=e;for(let r=0;r{let i=s.indexOf(":");if(i===-1)return`${n}${y}@`;let a=Y(s,0,i);return`${n}${a}:${y}@`}]);return t=E(x,Bt,[t,(r,n,s,i)=>Wt(n,s,i)?r:`${n}${s}:${y}@`]),t=E(x,/([?#&;])([-a-z0-9_.%\\[\\]]+)=([^&#;\\s]*)/gi,[t,(r,n,s,i)=>{let a=Xt(s);return Vt.has(se(a))||Ne(a)?`${n}${s}=${y}`:r}]),t=E(x,/(^|[^a-z0-9_-])((?:set-cookie|cookie)\\s*:\\s*)[^\\r\\n]*/gi,[t,(r,n,s)=>`${n}${s}${y}`]),t=E(x,/\\b(authorization\\s*[:=]\\s*)[^\\r\\n]*/gi,[t,(r,n)=>`${n}${y}`]),t=E(x,/\\b(bearer|basic)(\\s+)(?:"[^"\\r\\n]*"|\'[^\'\\r\\n]*\'|[a-z0-9._~+/=-]+)/gi,[t,(r,n,s)=>`${n}${s}${y}`]),t=Ie(t,/(["\'])([_$a-z][a-z0-9_.$-]*)\\1(\\s*[:=]\\s*)/gi,2),t=Ie(t,/(^|[^a-z0-9_.$-])([_$a-z][a-z0-9_.$-]*)(\\s*[:=]\\s*)/gi,2,1),t}var qt=2048;var Hs=64*1024,Jt=256,Zt="https://veryfront.com/docs/errors/",Pe="...[truncated]",ae="unknown-error";function He(e,t){if(e.length<=t)return e;let r=Math.max(0,t-Pe.length);return`${Qt(e,r)}${Pe}`}function Qt(e,t){let r=e.slice(0,t),n=r.charCodeAt(r.length-1);return n>=55296&&n<=56319&&(r=r.slice(0,-1)),r}function er(e){let t="";for(let r=0;r=55296&&n<=56319){let s=e.charCodeAt(r+1);s>=56320&&s<=57343?(t+=e.slice(r,r+2),r++):t+="\\uFFFD";continue}t+=n>=56320&&n<=57343?"\\uFFFD":e.charAt(r)}return t}function C(e){return typeof e!="string"?y:He(ie(e),qt)}function tr(e){let t=typeof e=="string"?ie(e):ae,r=He(t||ae,Jt),n=er(r);return n==="."||n===".."?ae:n}function K(e){let t=encodeURIComponent(tr(e));return`${Zt}${t}`}var rr=Object.freeze,nr=Object.getOwnPropertyDescriptors,ve=Number.isFinite,ke=new WeakSet,or=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function o(e){let t={...e},r={...t,create(n){let s=n?.message,i=n?.detail,a=n?.cause,u=n?.instance,d=n?.context,c=n?.status??t.status;return new ce(s||i||t.title,{slug:t.slug,category:t.category,status:c,title:t.title,suggestion:t.suggestion,exitCode:t.exitCode,detail:i,cause:a,instance:u,context:d})}};return rr(r)}var ce=class extends Error{constructor(r,n){super(r);m(this,"slug");m(this,"category");m(this,"status");m(this,"title");m(this,"suggestion");m(this,"exitCode");m(this,"detail");m(this,"cause");m(this,"instance");m(this,"context");ke.add(this),this.name="VeryfrontError",this.slug=n.slug,this.category=n.category,this.status=n.status,this.title=n.title,this.suggestion=n.suggestion,this.exitCode=n.exitCode,this.detail=n.detail,this.cause=n.cause,this.instance=n.instance,this.context=n.context}toRFC9457(){let r=Ue(this);return r?{type:K(r.slug),title:C(r.title),status:r.status,detail:r.detail===void 0?void 0:C(r.detail),instance:r.instance===void 0?void 0:C(r.instance),category:r.category,suggestion:r.suggestion===void 0?void 0:C(r.suggestion),cause:typeof r.cause=="string"?C(r.cause):void 0}:{type:K("unknown-error"),title:"Unknown/unclassified error",status:500,category:"GENERAL"}}getDocsUrl(){let r=Ue(this);return K(r?.slug??"unknown-error")}};function $e(e){return typeof e=="object"&&e!==null&&ke.has(e)}function Ue(e){return $e(e)?sr(e):null}function sr(e){try{if(!$e(e))return null;let t=nr(e),r=J=>{let b=t[J];return b&&"value"in b?b.value:void 0},n=r("slug"),s=r("category"),i=r("status"),a=r("title"),u=r("message"),d=r("suggestion"),c=r("exitCode"),g=r("detail"),f=r("cause"),R=r("instance"),P=r("context"),h=r("stack");return typeof n!="string"||!or.has(s)||typeof i!="number"||!ve(i)||typeof a!="string"||typeof u!="string"||d!==void 0&&typeof d!="string"||c!==void 0&&(typeof c!="number"||!ve(c))||g!==void 0&&typeof g!="string"||R!==void 0&&typeof R!="string"||h!==void 0&&typeof h!="string"?null:{slug:n,category:s,status:i,title:a,message:u,suggestion:d,exitCode:c,detail:g,cause:f,instance:R,context:P,stack:h}}catch{return null}}var ir=o({slug:"config-not-found",category:"CONFIG",status:404,title:"Configuration file not found",suggestion:"Create veryfront.config.js, veryfront.config.ts, or veryfront.config.mjs in the project root"}),ar=o({slug:"config-invalid",category:"CONFIG",status:400,title:"Invalid configuration format",suggestion:"Check the reported configuration path and validation details"}),cr=o({slug:"config-parse-error",category:"CONFIG",status:400,title:"Failed to parse configuration",suggestion:"Ensure your configuration file contains valid JavaScript or TypeScript"}),ur=o({slug:"config-validation-error",category:"CONFIG",status:422,title:"Configuration validation failed",suggestion:"Check the configuration against the schema requirements"}),lr=o({slug:"config-type-error",category:"CONFIG",status:400,title:"Configuration type mismatch",suggestion:"Ensure configuration values match expected types"}),dr=o({slug:"import-map-invalid",category:"CONFIG",status:400,title:"Invalid import map configuration",suggestion:"Check your import map syntax and paths"}),gr=o({slug:"cors-config-invalid",category:"CONFIG",status:400,title:"Invalid CORS configuration",suggestion:"Review CORS settings in your configuration"}),fr=o({slug:"config-validation-failed",category:"CONFIG",status:400,title:"Configuration validation failed",suggestion:"Check configuration values against requirements"}),pr=o({slug:"webhook-config-invalid",category:"CONFIG",status:400,title:"Invalid webhook configuration",suggestion:"Check webhook definition fields, target settings, and eventFilter conditions"}),yr=o({slug:"schedule-config-invalid",category:"CONFIG",status:400,title:"Invalid schedule configuration",suggestion:"Check schedule definition fields, cron expression, target settings, and positive-integer limits"}),mr=o({slug:"trigger-config-invalid",category:"CONFIG",status:400,title:"Invalid trigger configuration",suggestion:"Check trigger ID format (lowercase, alphanumeric, dots/slashes/hyphens) and ensure all input values are JSON-serializable"}),Ve={"config-not-found":ir,"config-invalid":ar,"config-parse-error":cr,"config-validation-error":ur,"config-type-error":lr,"import-map-invalid":dr,"cors-config-invalid":gr,"config-validation-failed":fr,"webhook-config-invalid":pr,"schedule-config-invalid":yr,"trigger-config-invalid":mr};var Er=o({slug:"build-failed",category:"BUILD",status:500,title:"Build process failed",suggestion:"Check the build output for specific errors"}),Rr=o({slug:"bundle-error",category:"BUILD",status:500,title:"Bundle generation failed",suggestion:"Review bundler output for details"}),hr=o({slug:"typescript-error",category:"BUILD",status:500,title:"TypeScript compilation error",suggestion:"Fix TypeScript errors shown in the output"}),_r=o({slug:"mdx-compile-error",category:"BUILD",status:500,title:"MDX compilation failed",suggestion:"Check your MDX file syntax"}),xr=o({slug:"asset-optimization-error",category:"BUILD",status:500,title:"Asset optimization failed",suggestion:"Check asset file formats and paths"}),Tr=o({slug:"ssg-generation-error",category:"BUILD",status:500,title:"Static site generation failed",suggestion:"Review SSG configuration and data fetching"}),Sr=o({slug:"sourcemap-error",category:"BUILD",status:500,title:"Source map generation failed",suggestion:"Check source map configuration"}),Cr=o({slug:"compilation-error",category:"BUILD",status:500,title:"Compilation failed",suggestion:"Review compiler output for specific errors"}),Fe={"build-failed":Er,"bundle-error":Rr,"typescript-error":hr,"mdx-compile-error":_r,"asset-optimization-error":xr,"ssg-generation-error":Tr,"sourcemap-error":Sr,"compilation-error":Cr};var Ar=o({slug:"hydration-mismatch",category:"RUNTIME",status:500,title:"Client/server hydration mismatch",suggestion:"Ensure server and client render the same content"}),br=o({slug:"render-error",category:"RUNTIME",status:500,title:"Component render failed",suggestion:"Check component for runtime errors"}),Or=o({slug:"component-error",category:"RUNTIME",status:500,title:"Component execution error",suggestion:"Review component logic and props"}),Ir=o({slug:"layout-not-found",category:"RUNTIME",status:404,title:"Layout component not found",suggestion:"Ensure layout file exists at the expected path"}),Nr=o({slug:"page-not-found",category:"RUNTIME",status:404,title:"Page component not found",suggestion:"Check that the page file exists in the routes directory"}),Dr=o({slug:"api-error",category:"RUNTIME",status:500,title:"API route handler error",suggestion:"Review API route handler for errors"}),wr=o({slug:"middleware-error",category:"RUNTIME",status:500,title:"Middleware execution error",suggestion:"Check middleware function for errors"}),Mr=o({slug:"trigger-target-not-found",category:"RUNTIME",status:404,title:"Trigger target not found",suggestion:"Ensure the referenced task or workflow ID is registered in the project"}),Lr=o({slug:"trigger-execution-failed",category:"RUNTIME",status:500,title:"Trigger target execution failed",suggestion:"Check the task or workflow for errors and review the trigger input"}),Pr=o({slug:"trigger-not-supported",category:"RUNTIME",status:501,title:"Trigger target type not supported in local runtime",suggestion:"Use a workflow or task target for local trigger runs; agent targets require the Cloud runtime"}),Be={"hydration-mismatch":Ar,"render-error":br,"component-error":Or,"layout-not-found":Ir,"page-not-found":Nr,"api-error":Dr,"middleware-error":wr,"trigger-target-not-found":Mr,"trigger-execution-failed":Lr,"trigger-not-supported":Pr};var Hr=o({slug:"route-conflict",category:"ROUTE",status:409,title:"Conflicting route definitions",suggestion:"Rename or reorganize conflicting route files"}),vr=o({slug:"invalid-route-file",category:"ROUTE",status:400,title:"Invalid route file structure",suggestion:"Ensure route file exports required functions"}),Ur=o({slug:"route-handler-invalid",category:"ROUTE",status:400,title:"Invalid route handler export",suggestion:"Export a valid handler function from the route file"}),kr=o({slug:"dynamic-route-error",category:"ROUTE",status:500,title:"Dynamic route parsing failed",suggestion:"Check dynamic route segment syntax"}),$r=o({slug:"route-params-error",category:"ROUTE",status:400,title:"Route parameters invalid",suggestion:"Validate route parameter values"}),Vr=o({slug:"api-route-error",category:"ROUTE",status:500,title:"API route definition error",suggestion:"Review API route configuration"}),Ge={"route-conflict":Hr,"invalid-route-file":vr,"route-handler-invalid":Ur,"dynamic-route-error":kr,"route-params-error":$r,"api-route-error":Vr};var Fr=o({slug:"module-not-found",category:"MODULE",status:404,title:"Module could not be resolved",suggestion:"Check the import path and ensure the module is installed"}),Br=o({slug:"import-resolution-error",category:"MODULE",status:500,title:"Import path resolution failed",suggestion:"Verify import paths and module configuration"}),Gr=o({slug:"circular-dependency",category:"MODULE",status:500,title:"Circular dependency detected",suggestion:"Refactor imports to break the circular dependency"}),jr=o({slug:"invalid-import",category:"MODULE",status:400,title:"Invalid import statement",suggestion:"Fix import syntax or path"}),zr=o({slug:"dependency-missing",category:"MODULE",status:404,title:"Required dependency not installed",suggestion:"Install the missing dependency with your package manager"}),Yr=o({slug:"version-mismatch",category:"MODULE",status:409,title:"Dependency version mismatch",suggestion:"Update dependencies to compatible versions"}),je={"module-not-found":Fr,"import-resolution-error":Br,"circular-dependency":Gr,"invalid-import":jr,"dependency-missing":zr,"version-mismatch":Yr};var Kr=o({slug:"port-in-use",category:"SERVER",status:409,title:"Server port already in use",suggestion:"Use a different port or stop the process using this port"}),Wr=o({slug:"server-start-error",category:"SERVER",status:500,title:"Server failed to start",suggestion:"Check server configuration and port availability"}),Xr=o({slug:"cache-error",category:"SERVER",status:500,title:"Cache operation failed",suggestion:"Clear the cache and try again"}),qr=o({slug:"file-watch-error",category:"SERVER",status:500,title:"File watcher error",suggestion:"Restart the development server"}),Jr=o({slug:"request-error",category:"SERVER",status:500,title:"HTTP request handling error",suggestion:"Check request handler and middleware"}),Zr=o({slug:"service-overloaded",category:"SERVER",status:503,title:"Service overloaded",suggestion:"Reduce load or scale up resources"}),Qr=o({slug:"project-execution-unavailable",category:"SERVER",status:503,title:"Project execution unavailable",suggestion:"Route the project to a dedicated isolated runtime"}),en=o({slug:"semaphore-timeout",category:"SERVER",status:503,title:"Semaphore acquire timeout",suggestion:"Reduce concurrency or increase the semaphore acquire timeout"}),tn=o({slug:"circuit-breaker-open",category:"SERVER",status:503,title:"Circuit breaker is open",suggestion:"Wait for the breaker reset timeout before retrying"}),rn=o({slug:"cache-path-mismatch",category:"SERVER",status:500,title:"Cache path mismatch",suggestion:"Clear the cache directory and rebuild"}),nn=o({slug:"network-error",category:"SERVER",status:502,title:"Network operation failed",suggestion:"Check network connectivity and retry"}),on=o({slug:"api-client-error",category:"SERVER",status:500,title:"API client request failed",suggestion:"Check API connectivity and authentication"}),sn=o({slug:"token-storage-error",category:"SERVER",status:500,title:"Token storage operation failed",suggestion:"Check token storage backend and credentials"}),an=o({slug:"cache-invariant-violation",category:"SERVER",status:500,title:"Cache path invariant violated",suggestion:"Clear the cache and rebuild"}),cn=o({slug:"release-not-found",category:"SERVER",status:404,title:"No active release found",suggestion:"Deploy the project to create a release for this environment"}),un=o({slug:"fallback-exhausted",category:"SERVER",status:500,title:"Primary and fallback operations both failed",suggestion:"Check service availability and connectivity"}),ln=o({slug:"rag-store-corrupt",category:"SERVER",status:500,title:"RAG store file is corrupt",suggestion:"Repair or move the store file aside, then retry; it was not overwritten"}),dn=o({slug:"rag-store-unavailable",category:"SERVER",status:500,title:"RAG store file is unavailable",suggestion:"Check storage availability, permissions, and concurrent operations, then retry"}),ze={"port-in-use":Kr,"server-start-error":Wr,"cache-error":Xr,"file-watch-error":qr,"request-error":Jr,"service-overloaded":Zr,"project-execution-unavailable":Qr,"semaphore-timeout":en,"circuit-breaker-open":tn,"cache-path-mismatch":rn,"network-error":nn,"api-client-error":on,"token-storage-error":sn,"cache-invariant-violation":an,"release-not-found":cn,"fallback-exhausted":un,"rag-store-corrupt":ln,"rag-store-unavailable":dn};var gn=o({slug:"client-boundary-violation",category:"BOUNDARY",status:400,title:"Client boundary rule violation",suggestion:"Add \'use client\' directive or move code to a client component"}),fn=o({slug:"server-only-in-client",category:"BOUNDARY",status:400,title:"Server-only code in client component",suggestion:"Move server-only code to a server component"}),pn=o({slug:"client-only-in-server",category:"BOUNDARY",status:400,title:"Client-only code in server component",suggestion:"Move client-only code to a client component"}),yn=o({slug:"invalid-use-client",category:"BOUNDARY",status:400,title:"Invalid \'use client\' directive",suggestion:"Place \'use client\' at the top of the file"}),mn=o({slug:"invalid-use-server",category:"BOUNDARY",status:400,title:"Invalid \'use server\' directive",suggestion:"Place \'use server\' at the top of the file or function"}),En=o({slug:"rsc-payload-error",category:"BOUNDARY",status:500,title:"RSC payload serialization error",suggestion:"Ensure props are serializable (no functions, symbols, etc.)"}),Rn=o({slug:"ssr-output-limit-exceeded",category:"BOUNDARY",status:500,title:"SSR output limit exceeded",suggestion:"Reduce the rendered HTML size or split the response into smaller pages"}),Ye={"client-boundary-violation":gn,"server-only-in-client":fn,"client-only-in-server":pn,"invalid-use-client":yn,"invalid-use-server":mn,"rsc-payload-error":En,"ssr-output-limit-exceeded":Rn};var hn=o({slug:"hmr-error",category:"DEV",status:500,title:"Hot module replacement error",suggestion:"Restart the development server"}),_n=o({slug:"dev-server-error",category:"DEV",status:500,title:"Development server error",suggestion:"Check the dev server logs and restart"}),xn=o({slug:"fast-refresh-error",category:"DEV",status:500,title:"Fast refresh failed",suggestion:"Save the file again or restart the dev server"}),Tn=o({slug:"error-overlay-error",category:"DEV",status:500,title:"Error overlay failed",suggestion:"Check browser console for details"}),Sn=o({slug:"source-map-error",category:"DEV",status:500,title:"Source map loading error",suggestion:"Rebuild or clear cache"}),Ke={"hmr-error":hn,"dev-server-error":_n,"fast-refresh-error":xn,"error-overlay-error":Tn,"source-map-error":Sn};var Cn=o({slug:"deployment-error",category:"DEPLOY",status:500,title:"Deployment process failed",suggestion:"Check deployment logs for details"}),An=o({slug:"platform-error",category:"DEPLOY",status:500,title:"Platform-specific error",suggestion:"Check platform documentation and requirements"}),bn=o({slug:"env-var-missing",category:"DEPLOY",status:500,title:"Required environment variable missing",suggestion:"Set the required environment variable"}),On=o({slug:"production-build-required",category:"DEPLOY",status:400,title:"Production build required",suggestion:"Run \'veryfront build\' before deploying"}),In=o({slug:"environment-not-found",category:"DEPLOY",status:404,title:"Deployment environment not found",suggestion:"Check environment names with: veryfront config"}),Nn=o({slug:"release-missing-version",category:"DEPLOY",status:500,title:"Release has no version",suggestion:"Try again or check the build logs in Studio"}),Dn=o({slug:"release-build-timeout",category:"DEPLOY",status:408,title:"Release build timed out",suggestion:"Try again or check the build logs in Studio"}),wn=o({slug:"deployment-verification-timeout",category:"DEPLOY",status:408,title:"Deployment verification timed out",suggestion:"Try again or check the deployment status in Studio"}),Mn=o({slug:"push-receipt-missing",category:"DEPLOY",status:400,title:"Push receipt not found",suggestion:"Run: veryfront push --branch main first"}),Ln=o({slug:"source-digest-mismatch",category:"DEPLOY",status:409,title:"Release source digest mismatch",suggestion:"Run veryfront push again to re-upload source files"}),Pn=o({slug:"preview-hostname-too-long",category:"DEPLOY",status:400,title:"Preview hostname too long",suggestion:"Use a shorter project slug or branch name"}),Hn=o({slug:"branch-not-found",category:"DEPLOY",status:404,title:"Branch not found",suggestion:"List branches in Studio or push a new one with: veryfront push --branch "}),We={"deployment-error":Cn,"platform-error":An,"env-var-missing":bn,"production-build-required":On,"environment-not-found":In,"release-missing-version":Nn,"release-build-timeout":Dn,"deployment-verification-timeout":wn,"push-receipt-missing":Mn,"source-digest-mismatch":Ln,"preview-hostname-too-long":Pn,"branch-not-found":Hn};var vn=o({slug:"agent-error",category:"AGENT",status:500,title:"Agent operation error",suggestion:"Check agent configuration and logs"}),Un=o({slug:"agent-not-found",category:"AGENT",status:404,title:"Agent not found",suggestion:"Verify the agent ID exists"}),kn=o({slug:"agent-timeout",category:"AGENT",status:408,title:"Agent operation timed out",suggestion:"Increase timeout or simplify the request"}),$n=o({slug:"agent-intent-error",category:"AGENT",status:400,title:"Agent intent parsing error",suggestion:"Rephrase the request more clearly"}),Vn=o({slug:"orchestration-error",category:"AGENT",status:500,title:"Multi-agent orchestration error",suggestion:"Check agent coordination logic"}),Fn=o({slug:"cost-limit-exceeded",category:"AGENT",status:429,title:"Cost limit exceeded",suggestion:"Wait for the budget period to reset or increase the limit"}),Bn=o({slug:"tool-id-conflict",category:"AGENT",status:409,title:"Tool ID conflict",suggestion:"Use a unique tool ID or rename one of the conflicting tools"}),Gn=o({slug:"durable-run-event-persistence-failed",category:"AGENT",status:500,title:"Durable run event persistence failed",suggestion:"Correct invalid or oversized event data, or retry after durable event storage recovers"}),Xe={"agent-error":vn,"agent-not-found":Un,"agent-timeout":kn,"agent-intent-error":$n,"orchestration-error":Vn,"cost-limit-exceeded":Fn,"tool-id-conflict":Bn,"durable-run-event-persistence-failed":Gn};var jn=o({slug:"unknown-error",category:"GENERAL",status:500,title:"Unknown/unclassified error",suggestion:"Check logs for more details"}),zn=o({slug:"authentication-required",category:"GENERAL",status:401,title:"Authentication required",suggestion:"Set VERYFRONT_API_TOKEN or run \'veryfront login\'"}),Yn=o({slug:"permission-denied",category:"GENERAL",status:403,title:"File/resource permission denied",suggestion:"Check file permissions and access rights"}),Kn=o({slug:"file-not-found",category:"GENERAL",status:404,title:"File not found",suggestion:"Verify the file path exists"}),Wn=o({slug:"resource-not-found",category:"GENERAL",status:404,title:"Requested resource not found",suggestion:"Verify the referenced resource ID or name exists"}),Xn=o({slug:"invalid-argument",category:"GENERAL",status:400,title:"Invalid function argument",suggestion:"Check argument types and values",exitCode:2}),qn=o({slug:"timeout-error",category:"GENERAL",status:408,title:"Operation timed out",suggestion:"Increase timeout or optimize the operation"}),Jn=o({slug:"initialization-error",category:"GENERAL",status:500,title:"Initialization failed",suggestion:"Check initialization requirements and dependencies"}),Zn=o({slug:"not-supported",category:"GENERAL",status:501,title:"Feature not supported",suggestion:"Check documentation for supported features"}),ue=o({slug:"security-violation",category:"GENERAL",status:403,title:"Security violation detected",suggestion:"Check for path traversal or unauthorized access attempts"}),Qn=o({slug:"input-validation-failed",category:"GENERAL",status:400,title:"Input validation failed",suggestion:"Check request input against validation rules"}),eo=o({slug:"project-source-empty",category:"GENERAL",status:400,title:"Project source is empty",suggestion:"Add project files or run \'veryfront init\'"}),qe={"unknown-error":jn,"authentication-required":zn,"permission-denied":Yn,"file-not-found":Kn,"resource-not-found":Wn,"invalid-argument":Xn,"timeout-error":qn,"initialization-error":Jn,"not-supported":Zn,"security-violation":ue,"input-validation-failed":Qn,"project-source-empty":eo};var Si=Ce(Ve,Fe,Be,Ge,je,ze,Ye,Ke,We,Xe,qe);var to=[{source:String.raw`]*>[\\s\\S]*?<\\/script>`,flags:"gi",name:"inline script"},{source:String.raw`javascript:`,flags:"gi",name:"javascript: URL"},{source:String.raw`\\bon\\w+\\s*=`,flags:"gi",name:"event handler attribute"},{source:String.raw`data:\\s*text\\/html`,flags:"gi",name:"data: HTML URL"}];function ro(){return to.map(({source:e,flags:t,name:r})=>({pattern:new RegExp(e,t),name:r}))}function no(){let e=globalThis;return e.__VERYFRONT_DEV__===!0||e.Deno?.env?.get?.("VERYFRONT_ENV")==="development"}function M(e,t={}){let{allowInlineScripts:r=!1,strict:n=!1,warn:s=!0}=t;for(let{pattern:i,name:a}of ro())if(!(r&&a==="inline script")&&(i.lastIndex=0,!!i.test(e)&&(s&&console.warn(`[Security] Suspicious ${a} detected in server HTML`),n||!no())))throw ue.create({detail:`Potentially unsafe HTML: ${a} detected`});return e}function L(e,t){let r=t==="root"?N:`rsc-slot-${t}`,n=e.getElementById(r);if(n)return n;let s=e.createElement("div");return s.id=r,e.body.appendChild(s),s}function oo(e,t){if(t.type!=="slot")return;let r=L(e,t.id);r.innerHTML=M(String(t.html??""))}function Je(e,t){let r=t.split(`\n`),n=r.pop()??"";for(let s of r){let i=s.trim();if(!i)continue;let a;try{a=JSON.parse(i)}catch(d){l.debug("[client-dom] malformed NDJSON line",{line:i,error:d instanceof Error?d.message:String(d)});continue}if(!a||typeof a!="object")continue;let u=a;if(u.type==="slot"){oo(e,u);try{ao(e,u.id||"root")}catch(d){l.debug("[client-dom] hydration optional failed",d)}}}return n}function so(e){return new Promise((t,r)=>{let n=()=>r(new DOMException("aborted","AbortError"));if(e.aborted){n();return}e.addEventListener("abort",n,{once:!0})})}async function Ze(e,t=document,r){let n="body"in e?e:null,s=n?.body??e;if(!s)return;n&&V(t,n.headers.get(k));let i=s.getReader(),a=new TextDecoder,u="",d=!1;try{for(;;){if(r?.aborted)throw new DOMException("aborted","AbortError");let c=i.read(),{done:g,value:f}=r?await Promise.race([c,so(r)]):await c;if(g){d=!0;break}u+=a.decode(f,{stream:!0}),u=Je(t,u)}u&&Je(t,`${u}\n`)}catch(c){throw c instanceof Error&&c.name==="AbortError"||l.debug("[client-dom] consumeNdjsonStream error",c),c}finally{try{await i.cancel()}catch(c){d||l.debug("[client-dom] reader.cancel failed",c)}try{i.releaseLock()}catch(c){l.debug("[client-dom] reader.releaseLock failed",c)}if(typeof s.cancel=="function")try{await s.cancel()}catch(c){l.debug("[client-dom] stream.cancel failed",c)}if(typeof n?.body?.cancel=="function")try{await n.body.cancel()}catch(c){l.debug("[client-dom] response.body.cancel failed",c)}}}function io(e,t){let r=L(e,t),n=[],s=i=>{let a=i;a.dataset?.clientRef&&n.push(a);for(let u of i.children)s(u)};return s(r),n}function ao(e,t){let r=io(e,t);for(let n of r){let s=n.dataset?.clientRef;s&&(n.dataset.hydrated="true",l.debug("[client-dom] marked for hydration",s))}}var co=new Set(["server","client","html","fragment"]);function Qe(e){if(!e)return[];try{let t=JSON.parse(e);return lo(t)?t.nodes:[]}catch{return[]}}async function de(e,t,r){return await Promise.all(e.map(n=>uo(n,t,r)))}async function uo(e,t,r){if(e.type==="html")return e.text??e.html??"";let n=await de(e.children??[],t,r);if(e.type==="fragment"||e.type==="server"&&!e.component)return t.createElement(t.Fragment,{},...n);if(e.type==="server")return t.createElement(e.component,e.props??{},...n);let s=await r(e.component);return s?t.createElement(s,e.props??{},...n):null}function lo(e){return!le(e)||e.version!==1||!Array.isArray(e.nodes)?!1:e.nodes.every(t=>et(t,0))}function et(e,t){return t>100||!le(e)||!co.has(e.type)||e.type==="html"&&typeof e.html!="string"&&typeof e.text!="string"||e.type==="client"&&typeof e.component!="string"||e.type==="server"&&e.component!==void 0&&typeof e.component!="string"||e.props!==void 0&&!le(e.props)?!1:e.children===void 0?!0:Array.isArray(e.children)&&e.children.every(r=>et(r,t+1))}function le(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function go(e){if(!e)return{};let t={};for(let[r,n]of Object.entries(e))t[r]=Array.isArray(n)?n.join("/"):n;return t}async function W(e,t,r=document){try{let n=Se(r);if(!n)return e;let i=(await import(n)).wrapForHydration;return typeof i!="function"?e:i(e,{params:go(t?.params),frontmatter:t?.frontmatter??{},data:t?.props??{}})}catch(n){return l.debug("router provider wrap failed",n),e}}var fo="Unknown dependency snapshot",po="export default null; // Unknown dependency snapshot",ge="__VF_DEPENDENCY_SNAPSHOT_RECOVERY_STARTED__";function yo(){return globalThis}async function mo(e){if(e.status!==409)return!1;try{let t=(await e.clone().text()).trim();return t===fo||t===po}catch{return!1}}async function A(e,t=()=>globalThis.location.reload()){if(!await mo(e))return!1;let r=yo();if(r[ge])return!0;r[ge]=!0;try{t()}catch{return delete r[ge],!1}return!0}async function X(e,t=globalThis.fetch,r=()=>globalThis.location.reload()){try{let n=new URL(e,"http://veryfront.local").searchParams.getAll("pins");if(n.length!==1||!n[0]?.startsWith("on:"))return!1;let s=await t(e,{cache:"no-store"});return await A(s,r)}catch{return!1}}var Eo=100;function Ro(e,t){if(globalThis.__VF_CLIENT_MOD_CACHE??(globalThis.__VF_CLIENT_MOD_CACHE=new Map),globalThis.__VF_CLIENT_MOD_CACHE.size>=Eo){let r=globalThis.__VF_CLIENT_MOD_CACHE.keys().next().value;r&&globalThis.__VF_CLIENT_MOD_CACHE.delete(r)}globalThis.__VF_CLIENT_MOD_CACHE.set(e,t)}function tt(e){let t=e.match(/^\\/app\\/(.+)#([\\w$.-]+)$/);if(t)return{rel:`/${t[1]||""}`,exportName:t[2]||"default"};let r=e.match(/^(\\/_veryfront\\/[^#]+)#([\\w$.-]+)$/);return r?{moduleUrl:r[1],exportName:r[2]||"default"}:(l.debug("hydrate: unrecognised client ref format, skipping",{ref:e}),null)}function ho(e){let t=e.dataset?.rscProps;if(!t)return{};try{let r=JSON.parse(t);return r&&typeof r=="object"&&!Array.isArray(r)?r:{}}catch(r){return l.debug("hydrate: invalid client boundary props, using empty props",r),{}}}function _o(e){return Qe(e.dataset?.rscChildren)}function xo(e){return"/_veryfront/rsc/manifest"}function To(e){return D(e)}async function So(e=document){try{let t=S(e),r=await fetch(xo(t),{headers:To(t)});return r.ok?await r.json():(await A(r),null)}catch{return null}}async function rt(e,t,r,n={}){let s=Co(e,t,r,n.releaseAssetModules),i=t.moduleUrl??t.rel;if(!i)return null;let a=`${i}#${e.hash??""}`;try{let u=globalThis.__VF_CLIENT_MOD_CACHE?.get(a);if(u)return u}catch(u){l.debug("hydrate: cache get failed",u)}if(!s)return null;try{let u=await(n.importModule??(d=>import(d)))(s);try{Ro(a,u)}catch(d){l.debug("hydrate: cache set failed",d)}return u}catch(u){return l.debug("hydrate: failed to import module",{moduleUrl:s,error:u}),await(n.recoverSnapshotFailure??X)(s),null}}function Co(e,t,r,n){if(t.moduleUrl)return B(t.moduleUrl,e.dependencyPinningCacheKey);if(!t.rel)return null;let s=e.graphIds?.client.find(i=>i.rel===t.rel)?.path;return G({strategy:r,rel:t.rel,absPath:s,version:e.hash,dependencyPinningCacheKey:e.dependencyPinningCacheKey,releaseAssetModules:n})}function Ao(e){let t=Array.from(e.querySelectorAll("[data-client-ref]")),r=new Set(t);return t.filter(n=>{let s=n.parentElement;for(;s;){if(r.has(s))return!1;s=s.parentElement}return!0})}async function nt(e=document){let t=null;try{t=await So(e)}catch(c){l.debug("hydrate: fetch manifest failed",c)}if(!t){l.debug("hydrate: no manifest");return}let r=Ao(e);try{let c=globalThis.__VF_MANIFEST_HASH;if(!r.some(f=>f.dataset?.hydrated!=="true")&&c&&t.hash&&c===t.hash)return}catch(c){l.debug("hydrate: hmr hash read failed",c)}if(r.length===0){try{globalThis.__VF_MANIFEST_HASH=t.hash??""}catch(c){l.debug("hydrate: set hash failed",c)}return}let n=S(e),s=F(n),i=n?.releaseAssetModules;try{if(globalThis.__VF_TEST_MODE__){globalThis.__VF_HYDRATE_CALLED=!0,globalThis.__VF_MANIFEST_HASH=t.hash??"";return}}catch(c){l.debug("hydrate: test mode flags failed",c)}let a=j(e,n?.reactVersion),[{default:u},{createRoot:d}]=await Promise.all([import(a.react),import(a.reactDomClient)]);for(let c of r){let g=c.dataset?.clientRef??"";if(!g||c.dataset?.hydrated==="true")continue;let f=tt(g);if(!f)continue;let R=await rt(t,f,s,{releaseAssetModules:i});if(!R)continue;let P=R[f.exportName]??R.default;if(typeof P=="function")try{let h=d(c),J=ho(c),b=_o(c),ot=await de(b,{Fragment:u.Fragment,createElement(H,Z,...v){return u.createElement(H,Z,...v)}},async H=>{let Z=t.modules.find(it=>it.id===H),v=t.components?.[H],ye=Z?.clientRef??(v?`${v}#default`:void 0);if(!ye)return null;let Q=tt(ye);if(!Q)return null;let ee=await rt(t,Q,s,{releaseAssetModules:i});if(!ee)return null;let me=ee[Q.exportName]??ee.default;return typeof me=="function"?me:null}),st=await W(u.createElement(P,J,...ot),n,e);h.render(st),c.dataset.hydrated="true"}catch(h){l.warn("hydrate: render failed",h)}}try{globalThis.__VF_MANIFEST_HASH=t.hash??""}catch(c){l.debug("hydrate: set hash failed (post)",c)}}var fe="data-vf-react-head-owner";var bo=2*1024*1024,ta=bo*2;var ra=64*1024,na=1024*1024,oa=1024*1024;var sa=new TextEncoder;async function Oo(){let e=S(document),t=j(document,e?.reactVersion),[r,n]=await Promise.all([import(t.react),import(t.reactDomClient)]);return{React:r,ReactDOM:n}}var Io=new Set(["SCRIPT","STYLE","NOSCRIPT","TEMPLATE"]);function pe(e){let t=e.getAttribute("style")??"";return e.hasAttribute("data-veryfront-head")||e.hasAttribute("hidden")||/(?:^|;)\\s*display\\s*:\\s*none(?:\\s*;|$)/i.test(t)||Io.has(e.tagName.toUpperCase())}function No(e,t){return e.find(r=>r.tagName.toUpperCase()==="DIV"&&!!r.getAttribute("class")?.trim()&&!pe(r))??t}function Do(e,t){return e===t}function wo(e,t){let r=document.createElement("div");r.setAttribute("data-veryfront-hydration-root","page");let n=e.find(s=>!pe(s));n?.parentNode===t?t.insertBefore(r,n):t.appendChild(r);for(let s of e)!pe(s)&&s.parentNode===t&&r.appendChild(s);return r}function Mo(e,t){for(let r of e){let n=[...r.hasAttribute(fe)?[r]:[],...r.querySelectorAll(`[${fe}]`)];for(let s of n)t.contains(s)||s.remove()}}function Lo(e,t,r=document){return!!t?.pagePath&&typeof e?.__veryfrontRenderPage=="function"&&!!r.getElementById("root")}function Po(e,t){return t?.pagePath?!1:!!e.getElementById(N)}function Ho(e=import.meta.url){try{return new URL(e,"http://veryfront.local").searchParams.get("hydrate")==="1"}catch{return!1}}function vo(e){return e==="rsc-module"}function Uo(e,t){return e?e.startsWith("?")?e:`?${e}`:""}function ko(e,t,r){return G({strategy:t,rel:e,releaseAssetModules:r?.releaseAssetModules,dependencyPinningCacheKey:r?.dependencyPinningCacheKey})}async function $o(e,t){try{let r=await fetch(I+"stream"+e,{headers:D(t)});if(!r.ok)return await A(r)?"snapshot-conflict":"failure";if(!r.body)return"failure";let n=new AbortController;return addEventListener("pagehide",()=>n.abort(),{once:!0}),await Ze(r,document,n.signal),"success"}catch(r){return l.debug("tryStream failed",r),"failure"}}async function q(){try{await nt(document)}catch(e){l.debug("hydration failed",e)}}async function Vo(e,t,r){try{let{React:n,ReactDOM:s}=await Oo(),i=ko(e,t,r);if(!i)return!1;l.debug("Loading component from:",i);let a;try{a=await import(i)}catch(R){throw await X(i),R}let u=a.default;if(typeof u!="function")return l.debug("Page component is not a function"),!1;let d=Array.from(document.body.children),c=No(d,document.body),g=Do(c,document.body)?wo(d,document.body):c;Mo(d,g);let f=await W(n.createElement(u,{}),r);return vo(t)?s.createRoot(g).render(f):s.hydrateRoot(g,f,{identifierPrefix:"vf",onRecoverableError:()=>{}}),l.debug("Page component hydrated successfully"),!0}catch(n){return l.error("Page hydration failed",n),!1}}async function Fo(e,t){try{let r=await fetch(I+"payload"+e,{headers:D(t)});if(!r.ok)return await A(r)?"snapshot-conflict":"failure";let n=await r.json();if(V(document,n?.dependencyPinningCacheKey),n?.slots){for(let[s,i]of Object.entries(n.slots))L(document,s).innerHTML=M(String(i||""));return"success"}return L(document,N).innerHTML=M(String(n?.html||"")),"success"}catch(r){return l.debug("payload fetch failed",r),"failure"}}async function Bo(){try{let e=S(document),t=Uo(globalThis.window?.location.search??"",e?.dependencyPinningCacheKey);if(Ho()){await q();return}let r=e?.pagePath,n=F(e);if(r){if(Lo(globalThis.window,e,document)){l.debug("Page renderer owns hydration");return}l.debug("Found page component in hydration data:",r),await Vo(r,n,e)&&l.debug("Client component hydrated successfully");return}if(!Po(document,e))return;let s=await $o(t,e);if(s==="snapshot-conflict")return;if(s==="success"){await q();return}let i=await Fo(t,e);if(i==="snapshot-conflict")return;if(i==="success"){await q();return}await q()}catch(e){l.error("boot failed",e)}}if(typeof document<"u"){let e=()=>{Bo()};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e,{once:!0}):e()}export{Bo as boot,ko as buildPageHydrationModuleUrl,Uo as buildRSCTransportQuery,Mo as retireAbandonedHeadOwnerMarkers,No as selectHydrationRoot,Po as shouldAttemptRSCTransport,Ho as shouldHydrateOnly,vo as shouldRenderPageComponent,Lo as shouldUsePageRendererHydration,Do as shouldWrapPageHydrationRoot};\n'; + 'var lt=Object.defineProperty;var dt=(e,t,r)=>t in e?lt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var m=(e,t,r)=>dt(e,typeof t!="symbol"?t+"":t,r);var Jo=Array.prototype.at,Zo=Array.prototype.filter,gt=Array.prototype.join,Qo=Array.prototype.map,ei=Array.prototype.pop,ft=Array.prototype.push,ti=Array.prototype.sort,he=Reflect.apply;function k(e,t){return he(gt,e,[t])}function O(e,t){he(ft,e,[t])}var pt="3.2.3",yt=Object.entries;function mt(e){let t=[];if(e?.external?.length&&O(t,`external=${k(e.external,",")}`),O(t,`target=${e?.target??"es2022"}`),e?.deps){let r=[],n=yt(e.deps);for(let i=0;it||r?.(n,...i)}debug(t,...r){this.log(0,console.debug,`[${this.prefix}] DEBUG: ${t}`,...r)}info(t,...r){this.log(1,console.log,`[${this.prefix}] ${t}`,...r)}warn(t,...r){this.log(2,console.warn,`[${this.prefix}] WARN: ${t}`,...r)}error(t,...r){this.log(3,console.error,`[${this.prefix}] ERROR: ${t}`,...r)}};function Dt(){if(typeof window>"u")return 2;let e=globalThis;return e.__VERYFRONT_DEV__||e.__RSC_DEV__?e.__VERYFRONT_DEBUG__||e.__RSC_DEBUG__?0:1:2}var F=Dt(),l=new T("RSC",F),Ii=new T("PREFETCH",F),Ni=new T("HYDRATE",F),Di=new T("VERYFRONT",F);var wt="veryfront-hydration-data";function se(e){try{let t=[...e.querySelectorAll(`[id="${wt}"]`)];if(t.length!==1)return null;let r=e.body;if(!r)return null;let n=t[0];return r.firstElementChild!==n&&n.parentElement!==r||n.tagName?.toLowerCase()!=="script"||n.getAttribute("type")?.trim().toLowerCase()!=="application/json"?null:n}catch{return null}}function S(e=document){try{let t=se(e);return t?JSON.parse(t.textContent||"{}"):null}catch(t){return l.debug("hydration data parse failed",t),null}}function B(e,t){if(!t?.startsWith("on:"))return!1;try{let r=se(e);if(!r)return!1;let n=JSON.parse(r.textContent||"{}");return n.dependencyPinningCacheKey=t,r.textContent=JSON.stringify(n),!0}catch(r){return l.debug("hydration dependency snapshot seed failed",r),!1}}function G(e){return e?.clientModuleStrategy?e.clientModuleStrategy:e?.dev?"fs":"rsc-module"}function Mt(e,t){if(!t)return e;let r=e.includes("?")?"&":"?";return`${e}${r}v=${encodeURIComponent(t)}`}function j(e,t){if(!t?.startsWith("on:"))return e;let r=e.indexOf("#"),n=r===-1?"":e.slice(r),i=r===-1?e:e.slice(0,r),s=i.indexOf("?"),a=s===-1?i:i.slice(0,s),u=new URLSearchParams(s===-1?"":i.slice(s+1));u.set("pins",t);let d=u.toString();return`${a}${d?`?${d}`:""}${n}`}function Lt(e,t){return Mt(`${Ce}${ne(e)}.js`,t)}function Pt(e,t,r){let n=t?`&v=${encodeURIComponent(t)}`:"";return j(`${N}module?rel=${encodeURIComponent(e)}${n}`,r)}function w(e){let t=e?.dependencyPinningCacheKey;return t?.startsWith("on:")?{[V]:t}:{}}function Ht(e){return e.replace(/^\\/+_vf_modules\\//,"").replace(/^\\/+/,"").replace(/\\.js$/,"")}var Ut=/\\.(tsx|ts|jsx|mdx|js)$/;function vt(e){let t=Ht(e),r=[e,t];return Ut.test(t)||r.push(`${t}.tsx`,`${t}.ts`,`${t}.jsx`,`${t}.mdx`,`${t}.js`),Array.from(new Set(r))}function kt(e,t){if(!e)return null;for(let r of vt(t)){let n=e[r];if(n)return n}return null}function z(e){if(e.strategy==="fs"){let r=e.absPath??e.rel;return r?j(Lt(r,e.version),e.dependencyPinningCacheKey):null}let t=kt(e.releaseAssetModules,e.rel);return t||Pt(e.rel,e.version,e.dependencyPinningCacheKey)}function Y(e=document,t=I){let r=oe(e);return{react:$("react",r)?"react":xe(t),reactDomClient:$("react-dom/client",r)?"react-dom/client":Te(t)}}function be(e=document){let t=oe(e);return $("veryfront/router",t)?"veryfront/router":null}var $t=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function Vt(e,...t){let r=Object.create(null),n=e.charAt(0).toUpperCase()+e.slice(1);for(let i of t)for(let[s,a]of Object.entries(i)){if(typeof a!="object"||a===null||!Object.hasOwn(a,"slug"))throw new Error(`${n} entry "${s}" must define a slug`);let u=a.slug;if(typeof u!="string")throw new Error(`${n} entry "${s}" must define a string slug`);if(u!==s)throw new Error(`${n} key "${s}" does not match entry slug "${u}"`);if(Object.hasOwn(r,s))throw new Error(`Duplicate ${e} slug "${s}"`);r[s]=a}return Object.freeze(r)}function Oe(...e){for(let t of e)for(let r of Object.values(t)){if(typeof r.slug!="string"||r.slug.length<3||r.slug.length>40||!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(r.slug))throw new TypeError(`Registered error slug must be 3-40 characters of lowercase kebab-case, got "${r.slug}"`);if(typeof r.category!="string"||!$t.has(r.category))throw new TypeError(`Registered error has unknown category "${r.category}"`);if(!Number.isInteger(r.status)||r.status<400||r.status>=600)throw new RangeError(`Registered error status must be an integer from 400 through 599, got ${r.status}`);if(typeof r.title!="string"||r.title.trim().length===0)throw new TypeError("Registered error title must be a non-empty string");if(r.suggestion!==void 0&&(typeof r.suggestion!="string"||r.suggestion.trim().length===0))throw new TypeError("Registered error suggestion must be non-empty when provided")}return Vt("error registry",...e)}var K={reset:"\\x1B[0m",dim:"\\x1B[2m",gray:"\\x1B[90m",red:"\\x1B[31m",green:"\\x1B[32m",yellow:"\\x1B[33m",blue:"\\x1B[34m",magenta:"\\x1B[35m",cyan:"\\x1B[36m"},Bi={debug:K.gray,info:K.green,warn:K.yellow,error:K.red};var y="[REDACTED]",E=Reflect.apply;var Ie=RegExp.prototype.exec,x=RegExp.prototype[Symbol.replace],ji=String.prototype.charCodeAt,Ne=String.prototype.slice,Ft=String.prototype.toLowerCase,Bt=/[^a-z0-9]/g;function ae(e){let t=E(Ft,e,[]);return E(x,Bt,[t,""])}function W(e,t,r){return r===void 0?E(Ne,e,[t]):E(Ne,e,[t,r])}var Gt=["password","passwd","pwd","passphrase","secret","clientsecret","token","apikey","accesskey","privatekey","credential","authorization","cookie","bearer","jwt","connectionstring","signature","sessionid","sid","otp","mfa","pin","salt","xsrf","csrf"],jt=512,zt=128,M=new Map;function Me(e){let t=e.length<=zt;if(t){let i=M.get(e);if(i!==void 0)return i}let r=ae(e),n=Gt.some(i=>r.includes(i));if(t){if(M.size>=jt){let i=M.keys().next().value;i!==void 0&&M.delete(i)}M.set(e,n)}return n}var Yt=["access_token","accesstoken","refresh_token","api_key","apikey","code","token","secret","client_secret","password","passwd","pwd","state","sig","signature","auth","x-amz-credential","x-amz-signature","x-amz-security-token","x-goog-credential","x-goog-signature"],Kt=new Set(Yt.map(ae)),Wt=/(\\b[a-z][a-z0-9+.-]*:\\/\\/|\\/\\/)([^/?#\\s]+)@/gi,Xt=/(\\b[a-z][a-z0-9+.-]*:\\/\\/|\\/\\/)([a-z0-9._~!$&\'()*+,;=%-]+):([^/?#@\\r\\n \\t]+[ \\t][^/?#@\\r\\n]*)@/gi,qt=3;function Jt(e){return e===" "||e==="\t"||e===","||e===";"||e==="&"||e==="?"||e==="#"}function Zt(e){if(!e)return!1;let t=e.charCodeAt(0);return t>=65&&t<=90||t>=97&&t<=122}function Le(e){return Zt(e)||e==="_"||e==="$"}function Qt(e){if(!e)return!1;let t=e.charCodeAt(0);return Le(e)||t>=48&&t<=57||e==="."||e==="-"}function Pe(e,t){let r=t,n=e[r]===\'"\'||e[r]==="\'"?e[r++]:"";if(!Le(e[r]))return!1;for(r++;Qt(e[r]);)r++;if(n){if(e[r]!==n)return!1;r++}for(;e[r]===" "||e[r]==="\t";)r++;return e[r]===":"||e[r]==="="}function He(e){return e==="\\r"||e===`\n`||e==="}"||e==="]"||Jt(e)}function Ue(e,t){let r=t;for(;r=e.length||Pe(e,r)}function er(e,t){let r=t,n=!0;if(e.startsWith(y,t)){let g=t+y.length;if(De(e,g))return{end:g,replacement:y};r=g,n=!1}let i=n&&(e[r]===\'"\'||e[r]==="\'"||e[r]==="`")?e[r]:"",s=!1,a=()=>i?`${i}${y}${s?i:""}`:y,u=[],d="",c=-1;for(let g=r;g0&&(f==="}"||f==="]")){if(u.at(-1)!==f)return{end:e.length,replacement:a()};if(u.pop(),g++,u.length===0&&De(e,g))return{end:g,replacement:a()};continue}if(u.length>0||!He(f)){g++;continue}let R=g;if(g=Ue(e,g),g>=e.length||Pe(e,g))return{end:R,replacement:a()}}return{end:e.length,replacement:a()}}function we(e,t,r,n){let i=0,s="";for(let a=E(Ie,t,[e]);a;a=E(Ie,t,[e])){let u=a[r];if(!Me(u))continue;let d=t.lastIndex,c=n===void 0?void 0:a[n],g=d+y.length;if((c==="?"||c==="&"||c===";")&&e.startsWith(y,d)&&e[g]==="#")continue;let f=er(e,d);s+=W(e,i,a.index),s+=a[0],s+=f.replacement,i=f.end,t.lastIndex=f.end}return i===0?e:s+W(e,i)}function tr(e,t,r){let n=r.search(/[ \\t]/);if(n<0)return!1;let i=`${t}:${W(r,0,n)}`,s=e==="//"?`https://${i}`:`${e}${i}`;try{let a=new URL(s);return a.username.length===0&&a.password.length===0}catch{return!1}}function rr(e){let t=e;for(let r=0;r{let s=i.indexOf(":");if(s===-1)return`${n}${y}@`;let a=W(i,0,s);return`${n}${a}:${y}@`}]);return t=E(x,Xt,[t,(r,n,i,s)=>tr(n,i,s)?r:`${n}${i}:${y}@`]),t=E(x,/([?#&;])([-a-z0-9_.%\\[\\]]+)=([^&#;\\s]*)/gi,[t,(r,n,i,s)=>{let a=rr(i);return Kt.has(ae(a))||Me(a)?`${n}${i}=${y}`:r}]),t=E(x,/(^|[^a-z0-9_-])((?:set-cookie|cookie)\\s*:\\s*)[^\\r\\n]*/gi,[t,(r,n,i)=>`${n}${i}${y}`]),t=E(x,/\\b(authorization\\s*[:=]\\s*)[^\\r\\n]*/gi,[t,(r,n)=>`${n}${y}`]),t=E(x,/\\b(bearer|basic)(\\s+)(?:"[^"\\r\\n]*"|\'[^\'\\r\\n]*\'|[a-z0-9._~+/=-]+)/gi,[t,(r,n,i)=>`${n}${i}${y}`]),t=we(t,/(["\'])([_$a-z][a-z0-9_.$-]*)\\1(\\s*[:=]\\s*)/gi,2),t=we(t,/(^|[^a-z0-9_.$-])([_$a-z][a-z0-9_.$-]*)(\\s*[:=]\\s*)/gi,2,1),t}var nr=2048;var Xi=64*1024,or=256,ir="https://veryfront.com/docs/errors/",ve="...[truncated]",ue="unknown-error";function ke(e,t){if(e.length<=t)return e;let r=Math.max(0,t-ve.length);return`${sr(e,r)}${ve}`}function sr(e,t){let r=e.slice(0,t),n=r.charCodeAt(r.length-1);return n>=55296&&n<=56319&&(r=r.slice(0,-1)),r}function ar(e){let t="";for(let r=0;r=55296&&n<=56319){let i=e.charCodeAt(r+1);i>=56320&&i<=57343?(t+=e.slice(r,r+2),r++):t+="\\uFFFD";continue}t+=n>=56320&&n<=57343?"\\uFFFD":e.charAt(r)}return t}function A(e){return typeof e!="string"?y:ke(ce(e),nr)}function cr(e){let t=typeof e=="string"?ce(e):ue,r=ke(t||ue,or),n=ar(r);return n==="."||n===".."?ue:n}function X(e){let t=encodeURIComponent(cr(e));return`${ir}${t}`}var ur=Object.freeze,lr=Object.getOwnPropertyDescriptors,$e=Number.isFinite,Fe=new WeakSet,dr=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function o(e){let t={...e},r={...t,create(n){let i=n?.message,s=n?.detail,a=n?.cause,u=n?.instance,d=n?.context,c=n?.status??t.status;return new le(i||s||t.title,{slug:t.slug,category:t.category,status:c,title:t.title,suggestion:t.suggestion,exitCode:t.exitCode,detail:s,cause:a,instance:u,context:d})}};return ur(r)}var le=class extends Error{constructor(r,n){super(r);m(this,"slug");m(this,"category");m(this,"status");m(this,"title");m(this,"suggestion");m(this,"exitCode");m(this,"detail");m(this,"cause");m(this,"instance");m(this,"context");Fe.add(this),this.name="VeryfrontError",this.slug=n.slug,this.category=n.category,this.status=n.status,this.title=n.title,this.suggestion=n.suggestion,this.exitCode=n.exitCode,this.detail=n.detail,this.cause=n.cause,this.instance=n.instance,this.context=n.context}toRFC9457(){let r=Ve(this);return r?{type:X(r.slug),title:A(r.title),status:r.status,detail:r.detail===void 0?void 0:A(r.detail),instance:r.instance===void 0?void 0:A(r.instance),category:r.category,suggestion:r.suggestion===void 0?void 0:A(r.suggestion),cause:typeof r.cause=="string"?A(r.cause):void 0}:{type:X("unknown-error"),title:"Unknown/unclassified error",status:500,category:"GENERAL"}}getDocsUrl(){let r=Ve(this);return X(r?.slug??"unknown-error")}};function Be(e){return typeof e=="object"&&e!==null&&Fe.has(e)}function Ve(e){return Be(e)?gr(e):null}function gr(e){try{if(!Be(e))return null;let t=lr(e),r=Q=>{let b=t[Q];return b&&"value"in b?b.value:void 0},n=r("slug"),i=r("category"),s=r("status"),a=r("title"),u=r("message"),d=r("suggestion"),c=r("exitCode"),g=r("detail"),f=r("cause"),R=r("instance"),H=r("context"),h=r("stack");return typeof n!="string"||!dr.has(i)||typeof s!="number"||!$e(s)||typeof a!="string"||typeof u!="string"||d!==void 0&&typeof d!="string"||c!==void 0&&(typeof c!="number"||!$e(c))||g!==void 0&&typeof g!="string"||R!==void 0&&typeof R!="string"||h!==void 0&&typeof h!="string"?null:{slug:n,category:i,status:s,title:a,message:u,suggestion:d,exitCode:c,detail:g,cause:f,instance:R,context:H,stack:h}}catch{return null}}var fr=o({slug:"config-not-found",category:"CONFIG",status:404,title:"Configuration file not found",suggestion:"Create veryfront.config.js, veryfront.config.ts, or veryfront.config.mjs in the project root"}),pr=o({slug:"config-invalid",category:"CONFIG",status:400,title:"Invalid configuration format",suggestion:"Check the reported configuration path and validation details"}),yr=o({slug:"config-parse-error",category:"CONFIG",status:400,title:"Failed to parse configuration",suggestion:"Ensure your configuration file contains valid JavaScript or TypeScript"}),mr=o({slug:"config-validation-error",category:"CONFIG",status:422,title:"Configuration validation failed",suggestion:"Check the configuration against the schema requirements"}),Er=o({slug:"config-type-error",category:"CONFIG",status:400,title:"Configuration type mismatch",suggestion:"Ensure configuration values match expected types"}),Rr=o({slug:"import-map-invalid",category:"CONFIG",status:400,title:"Invalid import map configuration",suggestion:"Check your import map syntax and paths"}),hr=o({slug:"cors-config-invalid",category:"CONFIG",status:400,title:"Invalid CORS configuration",suggestion:"Review CORS settings in your configuration"}),_r=o({slug:"config-validation-failed",category:"CONFIG",status:400,title:"Configuration validation failed",suggestion:"Check configuration values against requirements"}),xr=o({slug:"webhook-config-invalid",category:"CONFIG",status:400,title:"Invalid webhook configuration",suggestion:"Check webhook definition fields, target settings, and eventFilter conditions"}),Tr=o({slug:"schedule-config-invalid",category:"CONFIG",status:400,title:"Invalid schedule configuration",suggestion:"Check schedule definition fields, cron expression, target settings, and positive-integer limits"}),Sr=o({slug:"trigger-config-invalid",category:"CONFIG",status:400,title:"Invalid trigger configuration",suggestion:"Check trigger ID format (lowercase, alphanumeric, dots/slashes/hyphens) and ensure all input values are JSON-serializable"}),Ge={"config-not-found":fr,"config-invalid":pr,"config-parse-error":yr,"config-validation-error":mr,"config-type-error":Er,"import-map-invalid":Rr,"cors-config-invalid":hr,"config-validation-failed":_r,"webhook-config-invalid":xr,"schedule-config-invalid":Tr,"trigger-config-invalid":Sr};var Ar=o({slug:"build-failed",category:"BUILD",status:500,title:"Build process failed",suggestion:"Check the build output for specific errors"}),Cr=o({slug:"bundle-error",category:"BUILD",status:500,title:"Bundle generation failed",suggestion:"Review bundler output for details"}),br=o({slug:"typescript-error",category:"BUILD",status:500,title:"TypeScript compilation error",suggestion:"Fix TypeScript errors shown in the output"}),Or=o({slug:"mdx-compile-error",category:"BUILD",status:500,title:"MDX compilation failed",suggestion:"Check your MDX file syntax"}),Ir=o({slug:"asset-optimization-error",category:"BUILD",status:500,title:"Asset optimization failed",suggestion:"Check asset file formats and paths"}),Nr=o({slug:"ssg-generation-error",category:"BUILD",status:500,title:"Static site generation failed",suggestion:"Review SSG configuration and data fetching"}),Dr=o({slug:"sourcemap-error",category:"BUILD",status:500,title:"Source map generation failed",suggestion:"Check source map configuration"}),wr=o({slug:"compilation-error",category:"BUILD",status:500,title:"Compilation failed",suggestion:"Review compiler output for specific errors"}),je={"build-failed":Ar,"bundle-error":Cr,"typescript-error":br,"mdx-compile-error":Or,"asset-optimization-error":Ir,"ssg-generation-error":Nr,"sourcemap-error":Dr,"compilation-error":wr};var Mr=o({slug:"hydration-mismatch",category:"RUNTIME",status:500,title:"Client/server hydration mismatch",suggestion:"Ensure server and client render the same content"}),Lr=o({slug:"render-error",category:"RUNTIME",status:500,title:"Component render failed",suggestion:"Check component for runtime errors"}),Pr=o({slug:"component-error",category:"RUNTIME",status:500,title:"Component execution error",suggestion:"Review component logic and props"}),Hr=o({slug:"layout-not-found",category:"RUNTIME",status:404,title:"Layout component not found",suggestion:"Ensure layout file exists at the expected path"}),Ur=o({slug:"page-not-found",category:"RUNTIME",status:404,title:"Page component not found",suggestion:"Check that the page file exists in the routes directory"}),vr=o({slug:"api-error",category:"RUNTIME",status:500,title:"API route handler error",suggestion:"Review API route handler for errors"}),kr=o({slug:"middleware-error",category:"RUNTIME",status:500,title:"Middleware execution error",suggestion:"Check middleware function for errors"}),$r=o({slug:"trigger-target-not-found",category:"RUNTIME",status:404,title:"Trigger target not found",suggestion:"Ensure the referenced task or workflow ID is registered in the project"}),Vr=o({slug:"trigger-execution-failed",category:"RUNTIME",status:500,title:"Trigger target execution failed",suggestion:"Check the task or workflow for errors and review the trigger input"}),Fr=o({slug:"trigger-not-supported",category:"RUNTIME",status:501,title:"Trigger target type not supported in local runtime",suggestion:"Use a workflow or task target for local trigger runs; agent targets require the Cloud runtime"}),ze={"hydration-mismatch":Mr,"render-error":Lr,"component-error":Pr,"layout-not-found":Hr,"page-not-found":Ur,"api-error":vr,"middleware-error":kr,"trigger-target-not-found":$r,"trigger-execution-failed":Vr,"trigger-not-supported":Fr};var Br=o({slug:"route-conflict",category:"ROUTE",status:409,title:"Conflicting route definitions",suggestion:"Rename or reorganize conflicting route files"}),Gr=o({slug:"invalid-route-file",category:"ROUTE",status:400,title:"Invalid route file structure",suggestion:"Ensure route file exports required functions"}),jr=o({slug:"route-handler-invalid",category:"ROUTE",status:400,title:"Invalid route handler export",suggestion:"Export a valid handler function from the route file"}),zr=o({slug:"dynamic-route-error",category:"ROUTE",status:500,title:"Dynamic route parsing failed",suggestion:"Check dynamic route segment syntax"}),Yr=o({slug:"route-params-error",category:"ROUTE",status:400,title:"Route parameters invalid",suggestion:"Validate route parameter values"}),Kr=o({slug:"api-route-error",category:"ROUTE",status:500,title:"API route definition error",suggestion:"Review API route configuration"}),Ye={"route-conflict":Br,"invalid-route-file":Gr,"route-handler-invalid":jr,"dynamic-route-error":zr,"route-params-error":Yr,"api-route-error":Kr};var Wr=o({slug:"module-not-found",category:"MODULE",status:404,title:"Module could not be resolved",suggestion:"Check the import path and ensure the module is installed"}),Xr=o({slug:"import-resolution-error",category:"MODULE",status:500,title:"Import path resolution failed",suggestion:"Verify import paths and module configuration"}),qr=o({slug:"circular-dependency",category:"MODULE",status:500,title:"Circular dependency detected",suggestion:"Refactor imports to break the circular dependency"}),Jr=o({slug:"invalid-import",category:"MODULE",status:400,title:"Invalid import statement",suggestion:"Fix import syntax or path"}),Zr=o({slug:"dependency-missing",category:"MODULE",status:404,title:"Required dependency not installed",suggestion:"Install the missing dependency with your package manager"}),Qr=o({slug:"version-mismatch",category:"MODULE",status:409,title:"Dependency version mismatch",suggestion:"Update dependencies to compatible versions"}),Ke={"module-not-found":Wr,"import-resolution-error":Xr,"circular-dependency":qr,"invalid-import":Jr,"dependency-missing":Zr,"version-mismatch":Qr};var en=o({slug:"port-in-use",category:"SERVER",status:409,title:"Server port already in use",suggestion:"Use a different port or stop the process using this port"}),tn=o({slug:"server-start-error",category:"SERVER",status:500,title:"Server failed to start",suggestion:"Check server configuration and port availability"}),rn=o({slug:"cache-error",category:"SERVER",status:500,title:"Cache operation failed",suggestion:"Clear the cache and try again"}),nn=o({slug:"file-watch-error",category:"SERVER",status:500,title:"File watcher error",suggestion:"Restart the development server"}),on=o({slug:"request-error",category:"SERVER",status:500,title:"HTTP request handling error",suggestion:"Check request handler and middleware"}),sn=o({slug:"service-overloaded",category:"SERVER",status:503,title:"Service overloaded",suggestion:"Reduce load or scale up resources"}),an=o({slug:"project-execution-unavailable",category:"SERVER",status:503,title:"Project execution unavailable",suggestion:"Route the project to a dedicated isolated runtime"}),cn=o({slug:"semaphore-timeout",category:"SERVER",status:503,title:"Semaphore acquire timeout",suggestion:"Reduce concurrency or increase the semaphore acquire timeout"}),un=o({slug:"circuit-breaker-open",category:"SERVER",status:503,title:"Circuit breaker is open",suggestion:"Wait for the breaker reset timeout before retrying"}),ln=o({slug:"cache-path-mismatch",category:"SERVER",status:500,title:"Cache path mismatch",suggestion:"Clear the cache directory and rebuild"}),dn=o({slug:"network-error",category:"SERVER",status:502,title:"Network operation failed",suggestion:"Check network connectivity and retry"}),gn=o({slug:"api-client-error",category:"SERVER",status:500,title:"API client request failed",suggestion:"Check API connectivity and authentication"}),fn=o({slug:"token-storage-error",category:"SERVER",status:500,title:"Token storage operation failed",suggestion:"Check token storage backend and credentials"}),pn=o({slug:"cache-invariant-violation",category:"SERVER",status:500,title:"Cache path invariant violated",suggestion:"Clear the cache and rebuild"}),yn=o({slug:"release-not-found",category:"SERVER",status:404,title:"No active release found",suggestion:"Deploy the project to create a release for this environment"}),mn=o({slug:"fallback-exhausted",category:"SERVER",status:500,title:"Primary and fallback operations both failed",suggestion:"Check service availability and connectivity"}),En=o({slug:"rag-store-corrupt",category:"SERVER",status:500,title:"RAG store file is corrupt",suggestion:"Repair or move the store file aside, then retry; it was not overwritten"}),Rn=o({slug:"rag-store-unavailable",category:"SERVER",status:500,title:"RAG store file is unavailable",suggestion:"Check storage availability, permissions, and concurrent operations, then retry"}),We={"port-in-use":en,"server-start-error":tn,"cache-error":rn,"file-watch-error":nn,"request-error":on,"service-overloaded":sn,"project-execution-unavailable":an,"semaphore-timeout":cn,"circuit-breaker-open":un,"cache-path-mismatch":ln,"network-error":dn,"api-client-error":gn,"token-storage-error":fn,"cache-invariant-violation":pn,"release-not-found":yn,"fallback-exhausted":mn,"rag-store-corrupt":En,"rag-store-unavailable":Rn};var hn=o({slug:"client-boundary-violation",category:"BOUNDARY",status:400,title:"Client boundary rule violation",suggestion:"Add \'use client\' directive or move code to a client component"}),_n=o({slug:"server-only-in-client",category:"BOUNDARY",status:400,title:"Server-only code in client component",suggestion:"Move server-only code to a server component"}),xn=o({slug:"client-only-in-server",category:"BOUNDARY",status:400,title:"Client-only code in server component",suggestion:"Move client-only code to a client component"}),Tn=o({slug:"invalid-use-client",category:"BOUNDARY",status:400,title:"Invalid \'use client\' directive",suggestion:"Place \'use client\' at the top of the file"}),Sn=o({slug:"invalid-use-server",category:"BOUNDARY",status:400,title:"Invalid \'use server\' directive",suggestion:"Place \'use server\' at the top of the file or function"}),An=o({slug:"rsc-payload-error",category:"BOUNDARY",status:500,title:"RSC payload serialization error",suggestion:"Ensure props are serializable (no functions, symbols, etc.)"}),Cn=o({slug:"ssr-output-limit-exceeded",category:"BOUNDARY",status:500,title:"SSR output limit exceeded",suggestion:"Reduce the rendered HTML size or split the response into smaller pages"}),Xe={"client-boundary-violation":hn,"server-only-in-client":_n,"client-only-in-server":xn,"invalid-use-client":Tn,"invalid-use-server":Sn,"rsc-payload-error":An,"ssr-output-limit-exceeded":Cn};var bn=o({slug:"hmr-error",category:"DEV",status:500,title:"Hot module replacement error",suggestion:"Restart the development server"}),On=o({slug:"dev-server-error",category:"DEV",status:500,title:"Development server error",suggestion:"Check the dev server logs and restart"}),In=o({slug:"fast-refresh-error",category:"DEV",status:500,title:"Fast refresh failed",suggestion:"Save the file again or restart the dev server"}),Nn=o({slug:"error-overlay-error",category:"DEV",status:500,title:"Error overlay failed",suggestion:"Check browser console for details"}),Dn=o({slug:"source-map-error",category:"DEV",status:500,title:"Source map loading error",suggestion:"Rebuild or clear cache"}),qe={"hmr-error":bn,"dev-server-error":On,"fast-refresh-error":In,"error-overlay-error":Nn,"source-map-error":Dn};var wn=o({slug:"deployment-error",category:"DEPLOY",status:500,title:"Deployment process failed",suggestion:"Check deployment logs for details"}),Mn=o({slug:"platform-error",category:"DEPLOY",status:500,title:"Platform-specific error",suggestion:"Check platform documentation and requirements"}),Ln=o({slug:"env-var-missing",category:"DEPLOY",status:500,title:"Required environment variable missing",suggestion:"Set the required environment variable"}),Pn=o({slug:"production-build-required",category:"DEPLOY",status:400,title:"Production build required",suggestion:"Run \'veryfront build\' before deploying"}),Hn=o({slug:"environment-not-found",category:"DEPLOY",status:404,title:"Deployment environment not found",suggestion:"Check environment names with: veryfront config"}),Un=o({slug:"release-missing-version",category:"DEPLOY",status:500,title:"Release has no version",suggestion:"Try again or check the build logs in Studio"}),vn=o({slug:"release-build-timeout",category:"DEPLOY",status:408,title:"Release build timed out",suggestion:"Try again or check the build logs in Studio"}),kn=o({slug:"deployment-verification-timeout",category:"DEPLOY",status:408,title:"Deployment verification timed out",suggestion:"Try again or check the deployment status in Studio"}),$n=o({slug:"push-receipt-missing",category:"DEPLOY",status:400,title:"Push receipt not found",suggestion:"Run: veryfront push --branch main first"}),Vn=o({slug:"source-digest-mismatch",category:"DEPLOY",status:409,title:"Release source digest mismatch",suggestion:"Run veryfront push again to re-upload source files"}),Fn=o({slug:"preview-hostname-too-long",category:"DEPLOY",status:400,title:"Preview hostname too long",suggestion:"Use a shorter project slug or branch name"}),Bn=o({slug:"branch-not-found",category:"DEPLOY",status:404,title:"Branch not found",suggestion:"List branches in Studio or push a new one with: veryfront push --branch "}),Je={"deployment-error":wn,"platform-error":Mn,"env-var-missing":Ln,"production-build-required":Pn,"environment-not-found":Hn,"release-missing-version":Un,"release-build-timeout":vn,"deployment-verification-timeout":kn,"push-receipt-missing":$n,"source-digest-mismatch":Vn,"preview-hostname-too-long":Fn,"branch-not-found":Bn};var Gn=o({slug:"agent-error",category:"AGENT",status:500,title:"Agent operation error",suggestion:"Check agent configuration and logs"}),jn=o({slug:"agent-not-found",category:"AGENT",status:404,title:"Agent not found",suggestion:"Verify the agent ID exists"}),zn=o({slug:"agent-timeout",category:"AGENT",status:408,title:"Agent operation timed out",suggestion:"Increase timeout or simplify the request"}),Yn=o({slug:"agent-intent-error",category:"AGENT",status:400,title:"Agent intent parsing error",suggestion:"Rephrase the request more clearly"}),Kn=o({slug:"orchestration-error",category:"AGENT",status:500,title:"Multi-agent orchestration error",suggestion:"Check agent coordination logic"}),Wn=o({slug:"cost-limit-exceeded",category:"AGENT",status:429,title:"Cost limit exceeded",suggestion:"Wait for the budget period to reset or increase the limit"}),Xn=o({slug:"tool-id-conflict",category:"AGENT",status:409,title:"Tool ID conflict",suggestion:"Use a unique tool ID or rename one of the conflicting tools"}),qn=o({slug:"durable-run-event-persistence-failed",category:"AGENT",status:500,title:"Durable run event persistence failed",suggestion:"Correct invalid or oversized event data, or retry after durable event storage recovers"}),Ze={"agent-error":Gn,"agent-not-found":jn,"agent-timeout":zn,"agent-intent-error":Yn,"orchestration-error":Kn,"cost-limit-exceeded":Wn,"tool-id-conflict":Xn,"durable-run-event-persistence-failed":qn};var Jn=o({slug:"unknown-error",category:"GENERAL",status:500,title:"Unknown/unclassified error",suggestion:"Check logs for more details"}),Zn=o({slug:"authentication-required",category:"GENERAL",status:401,title:"Authentication required",suggestion:"Set VERYFRONT_API_TOKEN or run \'veryfront login\'"}),Qn=o({slug:"permission-denied",category:"GENERAL",status:403,title:"File/resource permission denied",suggestion:"Check file permissions and access rights"}),eo=o({slug:"file-not-found",category:"GENERAL",status:404,title:"File not found",suggestion:"Verify the file path exists"}),to=o({slug:"resource-not-found",category:"GENERAL",status:404,title:"Requested resource not found",suggestion:"Verify the referenced resource ID or name exists"}),ro=o({slug:"invalid-argument",category:"GENERAL",status:400,title:"Invalid function argument",suggestion:"Check argument types and values",exitCode:2}),no=o({slug:"timeout-error",category:"GENERAL",status:408,title:"Operation timed out",suggestion:"Increase timeout or optimize the operation"}),oo=o({slug:"initialization-error",category:"GENERAL",status:500,title:"Initialization failed",suggestion:"Check initialization requirements and dependencies"}),io=o({slug:"not-supported",category:"GENERAL",status:501,title:"Feature not supported",suggestion:"Check documentation for supported features"}),de=o({slug:"security-violation",category:"GENERAL",status:403,title:"Security violation detected",suggestion:"Check for path traversal or unauthorized access attempts"}),so=o({slug:"input-validation-failed",category:"GENERAL",status:400,title:"Input validation failed",suggestion:"Check request input against validation rules"}),ao=o({slug:"project-source-empty",category:"GENERAL",status:400,title:"Project source is empty",suggestion:"Add project files or run \'veryfront init\'"}),Qe={"unknown-error":Jn,"authentication-required":Zn,"permission-denied":Qn,"file-not-found":eo,"resource-not-found":to,"invalid-argument":ro,"timeout-error":no,"initialization-error":oo,"not-supported":io,"security-violation":de,"input-validation-failed":so,"project-source-empty":ao};var vs=Oe(Ge,je,ze,Ye,Ke,We,Xe,qe,Je,Ze,Qe);var co=[{source:String.raw`]*>[\\s\\S]*?<\\/script>`,flags:"gi",name:"inline script"},{source:String.raw`javascript:`,flags:"gi",name:"javascript: URL"},{source:String.raw`\\bon\\w+\\s*=`,flags:"gi",name:"event handler attribute"},{source:String.raw`data:\\s*text\\/html`,flags:"gi",name:"data: HTML URL"}];function uo(){return co.map(({source:e,flags:t,name:r})=>({pattern:new RegExp(e,t),name:r}))}function lo(){let e=globalThis;return e.__VERYFRONT_DEV__===!0||e.Deno?.env?.get?.("VERYFRONT_ENV")==="development"}function L(e,t={}){let{allowInlineScripts:r=!1,strict:n=!1,warn:i=!0}=t;for(let{pattern:s,name:a}of uo())if(!(r&&a==="inline script")&&(s.lastIndex=0,!!s.test(e)&&(i&&console.warn(`[Security] Suspicious ${a} detected in server HTML`),n||!lo())))throw de.create({detail:`Potentially unsafe HTML: ${a} detected`});return e}function P(e,t){let r=t==="root"?D:`rsc-slot-${t}`,n=e.getElementById(r);if(n)return n;let i=e.createElement("div");return i.id=r,e.body.appendChild(i),i}function go(e,t){if(t.type!=="slot")return;let r=P(e,t.id);r.innerHTML=L(String(t.html??""))}function et(e,t){let r=t.split(`\n`),n=r.pop()??"";for(let i of r){let s=i.trim();if(!s)continue;let a;try{a=JSON.parse(s)}catch(d){l.debug("[client-dom] malformed NDJSON line",{line:s,error:d instanceof Error?d.message:String(d)});continue}if(!a||typeof a!="object")continue;let u=a;if(u.type==="slot"){go(e,u);try{yo(e,u.id||"root")}catch(d){l.debug("[client-dom] hydration optional failed",d)}}}return n}function fo(e){return new Promise((t,r)=>{let n=()=>r(new DOMException("aborted","AbortError"));if(e.aborted){n();return}e.addEventListener("abort",n,{once:!0})})}async function tt(e,t=document,r){let n="body"in e?e:null,i=n?.body??e;if(!i)return;n&&B(t,n.headers.get(V));let s=i.getReader(),a=new TextDecoder,u="",d=!1;try{for(;;){if(r?.aborted)throw new DOMException("aborted","AbortError");let c=s.read(),{done:g,value:f}=r?await Promise.race([c,fo(r)]):await c;if(g){d=!0;break}u+=a.decode(f,{stream:!0}),u=et(t,u)}u&&et(t,`${u}\n`)}catch(c){throw c instanceof Error&&c.name==="AbortError"||l.debug("[client-dom] consumeNdjsonStream error",c),c}finally{try{await s.cancel()}catch(c){d||l.debug("[client-dom] reader.cancel failed",c)}try{s.releaseLock()}catch(c){l.debug("[client-dom] reader.releaseLock failed",c)}if(typeof i.cancel=="function")try{await i.cancel()}catch(c){l.debug("[client-dom] stream.cancel failed",c)}if(typeof n?.body?.cancel=="function")try{await n.body.cancel()}catch(c){l.debug("[client-dom] response.body.cancel failed",c)}}}function po(e,t){let r=P(e,t),n=[],i=s=>{let a=s;a.dataset?.clientRef&&n.push(a);for(let u of s.children)i(u)};return i(r),n}function yo(e,t){let r=po(e,t);for(let n of r){let i=n.dataset?.clientRef;i&&(n.dataset.hydrated="true",l.debug("[client-dom] marked for hydration",i))}}var mo=new Set(["server","client","html","fragment"]);function rt(e){if(!e)return[];try{let t=JSON.parse(e);return Ro(t)?t.nodes:[]}catch{return[]}}async function fe(e,t,r){return await Promise.all(e.map(n=>Eo(n,t,r)))}async function Eo(e,t,r){if(e.type==="html")return e.text??e.html??"";let n=await fe(e.children??[],t,r);if(e.type==="fragment"||e.type==="server"&&!e.component)return t.createElement(t.Fragment,{},...n);if(e.type==="server")return t.createElement(e.component,e.props??{},...n);let i=await r(e.component);return i?t.createElement(i,e.props??{},...n):null}function Ro(e){return!ge(e)||e.version!==1||!Array.isArray(e.nodes)?!1:e.nodes.every(t=>nt(t,0))}function nt(e,t){return t>100||!ge(e)||!mo.has(e.type)||e.type==="html"&&typeof e.html!="string"&&typeof e.text!="string"||e.type==="client"&&typeof e.component!="string"||e.type==="server"&&e.component!==void 0&&typeof e.component!="string"||e.props!==void 0&&!ge(e.props)?!1:e.children===void 0?!0:Array.isArray(e.children)&&e.children.every(r=>nt(r,t+1))}function ge(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function ho(e){if(!e)return{};let t={};for(let[r,n]of Object.entries(e))t[r]=Array.isArray(n)?n.join("/"):n;return t}async function q(e,t,r=document){try{let n=be(r);if(!n)return e;let s=(await import(n)).wrapForHydration;return typeof s!="function"?e:s(e,{params:ho(t?.params),frontmatter:t?.frontmatter??{},data:t?.props??{}})}catch(n){return l.debug("router provider wrap failed",n),e}}var _o="Unknown dependency snapshot",xo="export default null; // Unknown dependency snapshot",pe="__VF_DEPENDENCY_SNAPSHOT_RECOVERY_STARTED__";function To(){return globalThis}async function So(e){if(e.status!==409)return!1;try{let t=(await e.clone().text()).trim();return t===_o||t===xo}catch{return!1}}async function C(e,t=()=>globalThis.location.reload()){if(!await So(e))return!1;let r=To();if(r[pe])return!0;r[pe]=!0;try{t()}catch{return delete r[pe],!1}return!0}async function J(e,t=globalThis.fetch,r=()=>globalThis.location.reload()){try{let n=new URL(e,"http://veryfront.local").searchParams.getAll("pins");if(n.length!==1||!n[0]?.startsWith("on:"))return!1;let i=await t(e,{cache:"no-store"});return await C(i,r)}catch{return!1}}var Ao=100;function Co(e,t){if(globalThis.__VF_CLIENT_MOD_CACHE??(globalThis.__VF_CLIENT_MOD_CACHE=new Map),globalThis.__VF_CLIENT_MOD_CACHE.size>=Ao){let r=globalThis.__VF_CLIENT_MOD_CACHE.keys().next().value;r&&globalThis.__VF_CLIENT_MOD_CACHE.delete(r)}globalThis.__VF_CLIENT_MOD_CACHE.set(e,t)}function ot(e){let t=e.match(/^\\/app\\/(.+)#([\\w$.-]+)$/);if(t)return{rel:`/${t[1]||""}`,exportName:t[2]||"default"};let r=e.match(/^(\\/_veryfront\\/[^#]+)#([\\w$.-]+)$/);return r?{moduleUrl:r[1],exportName:r[2]||"default"}:(l.debug("hydrate: unrecognised client ref format, skipping",{ref:e}),null)}function bo(e){let t=e.dataset?.rscProps;if(!t)return{};try{let r=JSON.parse(t);return r&&typeof r=="object"&&!Array.isArray(r)?r:{}}catch(r){return l.debug("hydrate: invalid client boundary props, using empty props",r),{}}}function Oo(e){return rt(e.dataset?.rscChildren)}function Io(e){return"/_veryfront/rsc/manifest"}function No(e){return w(e)}async function Do(e=document){try{let t=S(e),r=await fetch(Io(t),{headers:No(t)});return r.ok?await r.json():(await C(r),null)}catch{return null}}async function it(e,t,r,n={}){let i=wo(e,t,r,n.releaseAssetModules),s=t.moduleUrl??t.rel;if(!s)return null;let a=`${s}#${e.hash??""}`;try{let u=globalThis.__VF_CLIENT_MOD_CACHE?.get(a);if(u)return u}catch(u){l.debug("hydrate: cache get failed",u)}if(!i)return null;try{let u=await(n.importModule??(d=>import(d)))(i);try{Co(a,u)}catch(d){l.debug("hydrate: cache set failed",d)}return u}catch(u){return l.debug("hydrate: failed to import module",{moduleUrl:i,error:u}),await(n.recoverSnapshotFailure??J)(i),null}}function wo(e,t,r,n){if(t.moduleUrl)return j(t.moduleUrl,e.dependencyPinningCacheKey);if(!t.rel)return null;let i=e.graphIds?.client.find(s=>s.rel===t.rel)?.path;return z({strategy:r,rel:t.rel,absPath:i,version:e.hash,dependencyPinningCacheKey:e.dependencyPinningCacheKey,releaseAssetModules:n})}function Mo(e){let t=Array.from(e.querySelectorAll("[data-client-ref]")),r=new Set(t);return t.filter(n=>{let i=n.parentElement;for(;i;){if(r.has(i))return!1;i=i.parentElement}return!0})}async function st(e=document){let t=null;try{t=await Do(e)}catch(c){l.debug("hydrate: fetch manifest failed",c)}if(!t){l.debug("hydrate: no manifest");return}let r=Mo(e);try{let c=globalThis.__VF_MANIFEST_HASH;if(!r.some(f=>f.dataset?.hydrated!=="true")&&c&&t.hash&&c===t.hash)return}catch(c){l.debug("hydrate: hmr hash read failed",c)}if(r.length===0){try{globalThis.__VF_MANIFEST_HASH=t.hash??""}catch(c){l.debug("hydrate: set hash failed",c)}return}let n=S(e),i=G(n),s=n?.releaseAssetModules;try{if(globalThis.__VF_TEST_MODE__){globalThis.__VF_HYDRATE_CALLED=!0,globalThis.__VF_MANIFEST_HASH=t.hash??"";return}}catch(c){l.debug("hydrate: test mode flags failed",c)}let a=Y(e,n?.reactVersion),[{default:u},{createRoot:d}]=await Promise.all([import(a.react),import(a.reactDomClient)]);for(let c of r){let g=c.dataset?.clientRef??"";if(!g||c.dataset?.hydrated==="true")continue;let f=ot(g);if(!f)continue;let R=await it(t,f,i,{releaseAssetModules:s});if(!R)continue;let H=R[f.exportName]??R.default;if(typeof H=="function")try{let h=d(c),Q=bo(c),b=Oo(c),at=await fe(b,{Fragment:u.Fragment,createElement(U,ee,...v){return u.createElement(U,ee,...v)}},async U=>{let ee=t.modules.find(ut=>ut.id===U),v=t.components?.[U],Ee=ee?.clientRef??(v?`${v}#default`:void 0);if(!Ee)return null;let te=ot(Ee);if(!te)return null;let re=await it(t,te,i,{releaseAssetModules:s});if(!re)return null;let Re=re[te.exportName]??re.default;return typeof Re=="function"?Re:null}),ct=await q(u.createElement(H,Q,...at),n,e);h.render(ct),c.dataset.hydrated="true"}catch(h){l.warn("hydrate: render failed",h)}}try{globalThis.__VF_MANIFEST_HASH=t.hash??""}catch(c){l.debug("hydrate: set hash failed (post)",c)}}var ye="data-vf-react-head-owner";var Lo=2*1024*1024,ya=Lo*2;var ma=64*1024,Ea=1024*1024,Ra=1024*1024;var ha=new TextEncoder;async function Po(){let e=S(document),t=Y(document,e?.reactVersion),[r,n]=await Promise.all([import(t.react),import(t.reactDomClient)]);return{React:r,ReactDOM:n}}var Ho=new Set(["SCRIPT","STYLE","NOSCRIPT","TEMPLATE"]);function me(e){let t=e.getAttribute("style")??"";return e.hasAttribute("data-veryfront-head")||e.hasAttribute("hidden")||/(?:^|;)\\s*display\\s*:\\s*none(?:\\s*;|$)/i.test(t)||Ho.has(e.tagName.toUpperCase())}function Uo(e,t){return e.find(r=>r.tagName.toUpperCase()==="DIV"&&!!r.getAttribute("class")?.trim()&&!me(r))??t}function vo(e,t){return e===t}function ko(e,t){let r=document.createElement("div");r.setAttribute("data-veryfront-hydration-root","page");let n=e.find(i=>!me(i));n?.parentNode===t?t.insertBefore(r,n):t.appendChild(r);for(let i of e)!me(i)&&i.parentNode===t&&r.appendChild(i);return r}function $o(e,t){for(let r of e){let n=[...r.hasAttribute(ye)?[r]:[],...r.querySelectorAll(`[${ye}]`)];for(let i of n)t.contains(i)||i.remove()}}function Vo(e,t,r=document){return!!t?.pagePath&&typeof e?.__veryfrontRenderPage=="function"&&!!r.getElementById("root")}function Fo(e,t){return t?.pagePath?!1:!!e.getElementById(D)}function Bo(e=import.meta.url){try{return new URL(e,"http://veryfront.local").searchParams.get("hydrate")==="1"}catch{return!1}}function Go(e){return e==="rsc-module"}function jo(e,t){return e?e.startsWith("?")?e:`?${e}`:""}function zo(e,t,r){return z({strategy:t,rel:e,releaseAssetModules:r?.releaseAssetModules,dependencyPinningCacheKey:r?.dependencyPinningCacheKey})}async function Yo(e,t){try{let r=await fetch(N+"stream"+e,{headers:w(t)});if(!r.ok)return await C(r)?"snapshot-conflict":"failure";if(!r.body)return"failure";let n=new AbortController;return addEventListener("pagehide",()=>n.abort(),{once:!0}),await tt(r,document,n.signal),"success"}catch(r){return l.debug("tryStream failed",r),"failure"}}async function Z(){try{await st(document)}catch(e){l.debug("hydration failed",e)}}async function Ko(e,t,r){try{let{React:n,ReactDOM:i}=await Po(),s=zo(e,t,r);if(!s)return!1;l.debug("Loading component from:",s);let a;try{a=await import(s)}catch(R){throw await J(s),R}let u=a.default;if(typeof u!="function")return l.debug("Page component is not a function"),!1;let d=Array.from(document.body.children),c=Uo(d,document.body),g=vo(c,document.body)?ko(d,document.body):c;$o(d,g);let f=await q(n.createElement(u,{}),r);return Go(t)?i.createRoot(g).render(f):i.hydrateRoot(g,f,{identifierPrefix:"vf",onRecoverableError:()=>{}}),l.debug("Page component hydrated successfully"),!0}catch(n){return l.error("Page hydration failed",n),!1}}async function Wo(e,t){try{let r=await fetch(N+"payload"+e,{headers:w(t)});if(!r.ok)return await C(r)?"snapshot-conflict":"failure";let n=await r.json();if(B(document,n?.dependencyPinningCacheKey),n?.slots){for(let[i,s]of Object.entries(n.slots))P(document,i).innerHTML=L(String(s||""));return"success"}return P(document,D).innerHTML=L(String(n?.html||"")),"success"}catch(r){return l.debug("payload fetch failed",r),"failure"}}async function Xo(){try{let e=S(document),t=jo(globalThis.window?.location.search??"",e?.dependencyPinningCacheKey);if(Bo()){await Z();return}let r=e?.pagePath,n=G(e);if(r){if(Vo(globalThis.window,e,document)){l.debug("Page renderer owns hydration");return}l.debug("Found page component in hydration data:",r),await Ko(r,n,e)&&l.debug("Client component hydrated successfully");return}if(!Fo(document,e))return;let i=await Yo(t,e);if(i==="snapshot-conflict")return;if(i==="success"){await Z();return}let s=await Wo(t,e);if(s==="snapshot-conflict")return;if(s==="success"){await Z();return}await Z()}catch(e){l.error("boot failed",e)}}if(typeof document<"u"){let e=()=>{Xo()};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e,{once:!0}):e()}export{Xo as boot,zo as buildPageHydrationModuleUrl,jo as buildRSCTransportQuery,$o as retireAbandonedHeadOwnerMarkers,Uo as selectHydrationRoot,Fo as shouldAttemptRSCTransport,Bo as shouldHydrateOnly,Go as shouldRenderPageComponent,Vo as shouldUsePageRendererHydration,vo as shouldWrapPageHydrationRoot};\n'; export const CLIENT_DOM_BUNDLE: string = - 'var xe=Object.defineProperty;var _e=(t,r,e)=>r in t?xe(t,r,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[r]=e;var E=(t,r,e)=>_e(t,typeof r!="symbol"?r+"":r,e);var he=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function Se(t,...r){let e=Object.create(null),o=t.charAt(0).toUpperCase()+t.slice(1);for(let s of r)for(let[i,a]of Object.entries(s)){if(typeof a!="object"||a===null||!Object.hasOwn(a,"slug"))throw new Error(`${o} entry "${i}" must define a slug`);let c=a.slug;if(typeof c!="string")throw new Error(`${o} entry "${i}" must define a string slug`);if(c!==i)throw new Error(`${o} key "${i}" does not match entry slug "${c}"`);if(Object.hasOwn(e,i))throw new Error(`Duplicate ${t} slug "${i}"`);e[i]=a}return Object.freeze(e)}function P(...t){for(let r of t)for(let e of Object.values(r)){if(typeof e.slug!="string"||e.slug.length<3||e.slug.length>40||!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(e.slug))throw new TypeError(`Registered error slug must be 3-40 characters of lowercase kebab-case, got "${e.slug}"`);if(typeof e.category!="string"||!he.has(e.category))throw new TypeError(`Registered error has unknown category "${e.category}"`);if(!Number.isInteger(e.status)||e.status<400||e.status>=600)throw new RangeError(`Registered error status must be an integer from 400 through 599, got ${e.status}`);if(typeof e.title!="string"||e.title.trim().length===0)throw new TypeError("Registered error title must be a non-empty string");if(e.suggestion!==void 0&&(typeof e.suggestion!="string"||e.suggestion.trim().length===0))throw new TypeError("Registered error suggestion must be non-empty when provided")}return Se("error registry",...t)}var I={reset:"\\x1B[0m",dim:"\\x1B[2m",gray:"\\x1B[90m",red:"\\x1B[31m",green:"\\x1B[32m",yellow:"\\x1B[33m",blue:"\\x1B[34m",magenta:"\\x1B[35m",cyan:"\\x1B[36m"},ln={debug:I.gray,info:I.green,warn:I.yellow,error:I.red};var p="[REDACTED]",m=Reflect.apply;var $=RegExp.prototype.exec,y=RegExp.prototype[Symbol.replace],dn=String.prototype.charCodeAt,k=String.prototype.slice,Ie=String.prototype.toLowerCase,Oe=/[^a-z0-9]/g;function D(t){let r=m(Ie,t,[]);return m(y,Oe,[r,""])}function O(t,r,e){return e===void 0?m(k,t,[r]):m(k,t,[r,e])}var Te=["password","passwd","pwd","passphrase","secret","clientsecret","token","apikey","accesskey","privatekey","credential","authorization","cookie","bearer","jwt","connectionstring","signature","sessionid","sid","otp","mfa","pin","salt","xsrf","csrf"],Ce=512,Ae=128,S=new Map;function F(t){let r=t.length<=Ae;if(r){let s=S.get(t);if(s!==void 0)return s}let e=D(t),o=Te.some(s=>e.includes(s));if(r){if(S.size>=Ce){let s=S.keys().next().value;s!==void 0&&S.delete(s)}S.set(t,o)}return o}var Ne=["access_token","accesstoken","refresh_token","api_key","apikey","code","token","secret","client_secret","password","passwd","pwd","state","sig","signature","auth","x-amz-credential","x-amz-signature","x-amz-security-token","x-goog-credential","x-goog-signature"],De=new Set(Ne.map(D)),be=/(\\b[a-z][a-z0-9+.-]*:\\/\\/|\\/\\/)([^/?#\\s]+)@/gi,Le=/(\\b[a-z][a-z0-9+.-]*:\\/\\/|\\/\\/)([a-z0-9._~!$&\'()*+,;=%-]+):([^/?#@\\r\\n \\t]+[ \\t][^/?#@\\r\\n]*)@/gi,Ue=3;function we(t){return t===" "||t==="\t"||t===","||t===";"||t==="&"||t==="?"||t==="#"}function ve(t){if(!t)return!1;let r=t.charCodeAt(0);return r>=65&&r<=90||r>=97&&r<=122}function H(t){return ve(t)||t==="_"||t==="$"}function Me(t){if(!t)return!1;let r=t.charCodeAt(0);return H(t)||r>=48&&r<=57||t==="."||t==="-"}function j(t,r){let e=r,o=t[e]===\'"\'||t[e]==="\'"?t[e++]:"";if(!H(t[e]))return!1;for(e++;Me(t[e]);)e++;if(o){if(t[e]!==o)return!1;e++}for(;t[e]===" "||t[e]==="\t";)e++;return t[e]===":"||t[e]==="="}function z(t){return t==="\\r"||t===`\n`||t==="}"||t==="]"||we(t)}function Y(t,r){let e=r;for(;e=t.length||j(t,e)}function Pe(t,r){let e=r,o=!0;if(t.startsWith(p,r)){let g=r+p.length;if(V(t,g))return{end:g,replacement:p};e=g,o=!1}let s=o&&(t[e]===\'"\'||t[e]==="\'"||t[e]==="`")?t[e]:"",i=!1,a=()=>s?`${s}${p}${i?s:""}`:p,c=[],d="",u=-1;for(let g=e;g0&&(f==="}"||f==="]")){if(c.at(-1)!==f)return{end:t.length,replacement:a()};if(c.pop(),g++,c.length===0&&V(t,g))return{end:g,replacement:a()};continue}if(c.length>0||!z(f)){g++;continue}let h=g;if(g=Y(t,g),g>=t.length||j(t,g))return{end:h,replacement:a()}}return{end:t.length,replacement:a()}}function G(t,r,e,o){let s=0,i="";for(let a=m($,r,[t]);a;a=m($,r,[t])){let c=a[e];if(!F(c))continue;let d=r.lastIndex,u=o===void 0?void 0:a[o],g=d+p.length;if((u==="?"||u==="&"||u===";")&&t.startsWith(p,d)&&t[g]==="#")continue;let f=Pe(t,d);i+=O(t,s,a.index),i+=a[0],i+=f.replacement,s=f.end,r.lastIndex=f.end}return s===0?t:i+O(t,s)}function $e(t,r,e){let o=e.search(/[ \\t]/);if(o<0)return!1;let s=`${r}:${O(e,0,o)}`,i=t==="//"?`https://${s}`:`${t}${s}`;try{let a=new URL(i);return a.username.length===0&&a.password.length===0}catch{return!1}}function ke(t){let r=t;for(let e=0;e{let i=s.indexOf(":");if(i===-1)return`${o}${p}@`;let a=O(s,0,i);return`${o}${a}:${p}@`}]);return r=m(y,Le,[r,(e,o,s,i)=>$e(o,s,i)?e:`${o}${s}:${p}@`]),r=m(y,/([?#&;])([-a-z0-9_.%\\[\\]]+)=([^&#;\\s]*)/gi,[r,(e,o,s,i)=>{let a=ke(s);return De.has(D(a))||F(a)?`${o}${s}=${p}`:e}]),r=m(y,/(^|[^a-z0-9_-])((?:set-cookie|cookie)\\s*:\\s*)[^\\r\\n]*/gi,[r,(e,o,s)=>`${o}${s}${p}`]),r=m(y,/\\b(authorization\\s*[:=]\\s*)[^\\r\\n]*/gi,[r,(e,o)=>`${o}${p}`]),r=m(y,/\\b(bearer|basic)(\\s+)(?:"[^"\\r\\n]*"|\'[^\'\\r\\n]*\'|[a-z0-9._~+/=-]+)/gi,[r,(e,o,s)=>`${o}${s}${p}`]),r=G(r,/(["\'])([_$a-z][a-z0-9_.$-]*)\\1(\\s*[:=]\\s*)/gi,2),r=G(r,/(^|[^a-z0-9_.$-])([_$a-z][a-z0-9_.$-]*)(\\s*[:=]\\s*)/gi,2,1),r}var Ve=2048;var Rn=64*1024,Ge=256,Fe="https://veryfront.com/docs/errors/",B="...[truncated]",L="unknown-error";function W(t,r){if(t.length<=r)return t;let e=Math.max(0,r-B.length);return`${He(t,e)}${B}`}function He(t,r){let e=t.slice(0,r),o=e.charCodeAt(e.length-1);return o>=55296&&o<=56319&&(e=e.slice(0,-1)),e}function je(t){let r="";for(let e=0;e=55296&&o<=56319){let s=t.charCodeAt(e+1);s>=56320&&s<=57343?(r+=t.slice(e,e+2),e++):r+="\\uFFFD";continue}r+=o>=56320&&o<=57343?"\\uFFFD":t.charAt(e)}return r}function x(t){return typeof t!="string"?p:W(b(t),Ve)}function ze(t){let r=typeof t=="string"?b(t):L,e=W(r||L,Ge),o=je(e);return o==="."||o===".."?L:o}function T(t){let r=encodeURIComponent(ze(t));return`${Fe}${r}`}var Ye=Object.freeze,Be=Object.getOwnPropertyDescriptors,K=Number.isFinite,X=new WeakSet,We=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function n(t){let r={...t},e={...r,create(o){let s=o?.message,i=o?.detail,a=o?.cause,c=o?.instance,d=o?.context,u=o?.status??r.status;return new U(s||i||r.title,{slug:r.slug,category:r.category,status:u,title:r.title,suggestion:r.suggestion,exitCode:r.exitCode,detail:i,cause:a,instance:c,context:d})}};return Ye(e)}var U=class extends Error{constructor(e,o){super(e);E(this,"slug");E(this,"category");E(this,"status");E(this,"title");E(this,"suggestion");E(this,"exitCode");E(this,"detail");E(this,"cause");E(this,"instance");E(this,"context");X.add(this),this.name="VeryfrontError",this.slug=o.slug,this.category=o.category,this.status=o.status,this.title=o.title,this.suggestion=o.suggestion,this.exitCode=o.exitCode,this.detail=o.detail,this.cause=o.cause,this.instance=o.instance,this.context=o.context}toRFC9457(){let e=q(this);return e?{type:T(e.slug),title:x(e.title),status:e.status,detail:e.detail===void 0?void 0:x(e.detail),instance:e.instance===void 0?void 0:x(e.instance),category:e.category,suggestion:e.suggestion===void 0?void 0:x(e.suggestion),cause:typeof e.cause=="string"?x(e.cause):void 0}:{type:T("unknown-error"),title:"Unknown/unclassified error",status:500,category:"GENERAL"}}getDocsUrl(){let e=q(this);return T(e?.slug??"unknown-error")}};function J(t){return typeof t=="object"&&t!==null&&X.has(t)}function q(t){return J(t)?Ke(t):null}function Ke(t){try{if(!J(t))return null;let r=Be(t),e=ye=>{let N=r[ye];return N&&"value"in N?N.value:void 0},o=e("slug"),s=e("category"),i=e("status"),a=e("title"),c=e("message"),d=e("suggestion"),u=e("exitCode"),g=e("detail"),f=e("cause"),h=e("instance"),Re=e("context"),A=e("stack");return typeof o!="string"||!We.has(s)||typeof i!="number"||!K(i)||typeof a!="string"||typeof c!="string"||d!==void 0&&typeof d!="string"||u!==void 0&&(typeof u!="number"||!K(u))||g!==void 0&&typeof g!="string"||h!==void 0&&typeof h!="string"||A!==void 0&&typeof A!="string"?null:{slug:o,category:s,status:i,title:a,message:c,suggestion:d,exitCode:u,detail:g,cause:f,instance:h,context:Re,stack:A}}catch{return null}}var qe=n({slug:"config-not-found",category:"CONFIG",status:404,title:"Configuration file not found",suggestion:"Create veryfront.config.js, veryfront.config.ts, or veryfront.config.mjs in the project root"}),Xe=n({slug:"config-invalid",category:"CONFIG",status:400,title:"Invalid configuration format",suggestion:"Check the reported configuration path and validation details"}),Je=n({slug:"config-parse-error",category:"CONFIG",status:400,title:"Failed to parse configuration",suggestion:"Ensure your configuration file contains valid JavaScript or TypeScript"}),Ze=n({slug:"config-validation-error",category:"CONFIG",status:422,title:"Configuration validation failed",suggestion:"Check the configuration against the schema requirements"}),Qe=n({slug:"config-type-error",category:"CONFIG",status:400,title:"Configuration type mismatch",suggestion:"Ensure configuration values match expected types"}),et=n({slug:"import-map-invalid",category:"CONFIG",status:400,title:"Invalid import map configuration",suggestion:"Check your import map syntax and paths"}),tt=n({slug:"cors-config-invalid",category:"CONFIG",status:400,title:"Invalid CORS configuration",suggestion:"Review CORS settings in your configuration"}),rt=n({slug:"config-validation-failed",category:"CONFIG",status:400,title:"Configuration validation failed",suggestion:"Check configuration values against requirements"}),nt=n({slug:"webhook-config-invalid",category:"CONFIG",status:400,title:"Invalid webhook configuration",suggestion:"Check webhook definition fields, target settings, and eventFilter conditions"}),ot=n({slug:"schedule-config-invalid",category:"CONFIG",status:400,title:"Invalid schedule configuration",suggestion:"Check schedule definition fields, cron expression, target settings, and positive-integer limits"}),st=n({slug:"trigger-config-invalid",category:"CONFIG",status:400,title:"Invalid trigger configuration",suggestion:"Check trigger ID format (lowercase, alphanumeric, dots/slashes/hyphens) and ensure all input values are JSON-serializable"}),Z={"config-not-found":qe,"config-invalid":Xe,"config-parse-error":Je,"config-validation-error":Ze,"config-type-error":Qe,"import-map-invalid":et,"cors-config-invalid":tt,"config-validation-failed":rt,"webhook-config-invalid":nt,"schedule-config-invalid":ot,"trigger-config-invalid":st};var it=n({slug:"build-failed",category:"BUILD",status:500,title:"Build process failed",suggestion:"Check the build output for specific errors"}),at=n({slug:"bundle-error",category:"BUILD",status:500,title:"Bundle generation failed",suggestion:"Review bundler output for details"}),ct=n({slug:"typescript-error",category:"BUILD",status:500,title:"TypeScript compilation error",suggestion:"Fix TypeScript errors shown in the output"}),ut=n({slug:"mdx-compile-error",category:"BUILD",status:500,title:"MDX compilation failed",suggestion:"Check your MDX file syntax"}),lt=n({slug:"asset-optimization-error",category:"BUILD",status:500,title:"Asset optimization failed",suggestion:"Check asset file formats and paths"}),gt=n({slug:"ssg-generation-error",category:"BUILD",status:500,title:"Static site generation failed",suggestion:"Review SSG configuration and data fetching"}),dt=n({slug:"sourcemap-error",category:"BUILD",status:500,title:"Source map generation failed",suggestion:"Check source map configuration"}),ft=n({slug:"compilation-error",category:"BUILD",status:500,title:"Compilation failed",suggestion:"Review compiler output for specific errors"}),Q={"build-failed":it,"bundle-error":at,"typescript-error":ct,"mdx-compile-error":ut,"asset-optimization-error":lt,"ssg-generation-error":gt,"sourcemap-error":dt,"compilation-error":ft};var pt=n({slug:"hydration-mismatch",category:"RUNTIME",status:500,title:"Client/server hydration mismatch",suggestion:"Ensure server and client render the same content"}),Et=n({slug:"render-error",category:"RUNTIME",status:500,title:"Component render failed",suggestion:"Check component for runtime errors"}),mt=n({slug:"component-error",category:"RUNTIME",status:500,title:"Component execution error",suggestion:"Review component logic and props"}),Rt=n({slug:"layout-not-found",category:"RUNTIME",status:404,title:"Layout component not found",suggestion:"Ensure layout file exists at the expected path"}),yt=n({slug:"page-not-found",category:"RUNTIME",status:404,title:"Page component not found",suggestion:"Check that the page file exists in the routes directory"}),xt=n({slug:"api-error",category:"RUNTIME",status:500,title:"API route handler error",suggestion:"Review API route handler for errors"}),_t=n({slug:"middleware-error",category:"RUNTIME",status:500,title:"Middleware execution error",suggestion:"Check middleware function for errors"}),ht=n({slug:"trigger-target-not-found",category:"RUNTIME",status:404,title:"Trigger target not found",suggestion:"Ensure the referenced task or workflow ID is registered in the project"}),St=n({slug:"trigger-execution-failed",category:"RUNTIME",status:500,title:"Trigger target execution failed",suggestion:"Check the task or workflow for errors and review the trigger input"}),It=n({slug:"trigger-not-supported",category:"RUNTIME",status:501,title:"Trigger target type not supported in local runtime",suggestion:"Use a workflow or task target for local trigger runs; agent targets require the Cloud runtime"}),ee={"hydration-mismatch":pt,"render-error":Et,"component-error":mt,"layout-not-found":Rt,"page-not-found":yt,"api-error":xt,"middleware-error":_t,"trigger-target-not-found":ht,"trigger-execution-failed":St,"trigger-not-supported":It};var Ot=n({slug:"route-conflict",category:"ROUTE",status:409,title:"Conflicting route definitions",suggestion:"Rename or reorganize conflicting route files"}),Tt=n({slug:"invalid-route-file",category:"ROUTE",status:400,title:"Invalid route file structure",suggestion:"Ensure route file exports required functions"}),Ct=n({slug:"route-handler-invalid",category:"ROUTE",status:400,title:"Invalid route handler export",suggestion:"Export a valid handler function from the route file"}),At=n({slug:"dynamic-route-error",category:"ROUTE",status:500,title:"Dynamic route parsing failed",suggestion:"Check dynamic route segment syntax"}),Nt=n({slug:"route-params-error",category:"ROUTE",status:400,title:"Route parameters invalid",suggestion:"Validate route parameter values"}),Dt=n({slug:"api-route-error",category:"ROUTE",status:500,title:"API route definition error",suggestion:"Review API route configuration"}),te={"route-conflict":Ot,"invalid-route-file":Tt,"route-handler-invalid":Ct,"dynamic-route-error":At,"route-params-error":Nt,"api-route-error":Dt};var bt=n({slug:"module-not-found",category:"MODULE",status:404,title:"Module could not be resolved",suggestion:"Check the import path and ensure the module is installed"}),Lt=n({slug:"import-resolution-error",category:"MODULE",status:500,title:"Import path resolution failed",suggestion:"Verify import paths and module configuration"}),Ut=n({slug:"circular-dependency",category:"MODULE",status:500,title:"Circular dependency detected",suggestion:"Refactor imports to break the circular dependency"}),wt=n({slug:"invalid-import",category:"MODULE",status:400,title:"Invalid import statement",suggestion:"Fix import syntax or path"}),vt=n({slug:"dependency-missing",category:"MODULE",status:404,title:"Required dependency not installed",suggestion:"Install the missing dependency with your package manager"}),Mt=n({slug:"version-mismatch",category:"MODULE",status:409,title:"Dependency version mismatch",suggestion:"Update dependencies to compatible versions"}),re={"module-not-found":bt,"import-resolution-error":Lt,"circular-dependency":Ut,"invalid-import":wt,"dependency-missing":vt,"version-mismatch":Mt};var Pt=n({slug:"port-in-use",category:"SERVER",status:409,title:"Server port already in use",suggestion:"Use a different port or stop the process using this port"}),$t=n({slug:"server-start-error",category:"SERVER",status:500,title:"Server failed to start",suggestion:"Check server configuration and port availability"}),kt=n({slug:"cache-error",category:"SERVER",status:500,title:"Cache operation failed",suggestion:"Clear the cache and try again"}),Vt=n({slug:"file-watch-error",category:"SERVER",status:500,title:"File watcher error",suggestion:"Restart the development server"}),Gt=n({slug:"request-error",category:"SERVER",status:500,title:"HTTP request handling error",suggestion:"Check request handler and middleware"}),Ft=n({slug:"service-overloaded",category:"SERVER",status:503,title:"Service overloaded",suggestion:"Reduce load or scale up resources"}),Ht=n({slug:"project-execution-unavailable",category:"SERVER",status:503,title:"Project execution unavailable",suggestion:"Route the project to a dedicated isolated runtime"}),jt=n({slug:"semaphore-timeout",category:"SERVER",status:503,title:"Semaphore acquire timeout",suggestion:"Reduce concurrency or increase the semaphore acquire timeout"}),zt=n({slug:"circuit-breaker-open",category:"SERVER",status:503,title:"Circuit breaker is open",suggestion:"Wait for the breaker reset timeout before retrying"}),Yt=n({slug:"cache-path-mismatch",category:"SERVER",status:500,title:"Cache path mismatch",suggestion:"Clear the cache directory and rebuild"}),Bt=n({slug:"network-error",category:"SERVER",status:502,title:"Network operation failed",suggestion:"Check network connectivity and retry"}),Wt=n({slug:"api-client-error",category:"SERVER",status:500,title:"API client request failed",suggestion:"Check API connectivity and authentication"}),Kt=n({slug:"token-storage-error",category:"SERVER",status:500,title:"Token storage operation failed",suggestion:"Check token storage backend and credentials"}),qt=n({slug:"cache-invariant-violation",category:"SERVER",status:500,title:"Cache path invariant violated",suggestion:"Clear the cache and rebuild"}),Xt=n({slug:"release-not-found",category:"SERVER",status:404,title:"No active release found",suggestion:"Deploy the project to create a release for this environment"}),Jt=n({slug:"fallback-exhausted",category:"SERVER",status:500,title:"Primary and fallback operations both failed",suggestion:"Check service availability and connectivity"}),Zt=n({slug:"rag-store-corrupt",category:"SERVER",status:500,title:"RAG store file is corrupt",suggestion:"Repair or move the store file aside, then retry; it was not overwritten"}),Qt=n({slug:"rag-store-unavailable",category:"SERVER",status:500,title:"RAG store file is unavailable",suggestion:"Check storage availability, permissions, and concurrent operations, then retry"}),ne={"port-in-use":Pt,"server-start-error":$t,"cache-error":kt,"file-watch-error":Vt,"request-error":Gt,"service-overloaded":Ft,"project-execution-unavailable":Ht,"semaphore-timeout":jt,"circuit-breaker-open":zt,"cache-path-mismatch":Yt,"network-error":Bt,"api-client-error":Wt,"token-storage-error":Kt,"cache-invariant-violation":qt,"release-not-found":Xt,"fallback-exhausted":Jt,"rag-store-corrupt":Zt,"rag-store-unavailable":Qt};var er=n({slug:"client-boundary-violation",category:"BOUNDARY",status:400,title:"Client boundary rule violation",suggestion:"Add \'use client\' directive or move code to a client component"}),tr=n({slug:"server-only-in-client",category:"BOUNDARY",status:400,title:"Server-only code in client component",suggestion:"Move server-only code to a server component"}),rr=n({slug:"client-only-in-server",category:"BOUNDARY",status:400,title:"Client-only code in server component",suggestion:"Move client-only code to a client component"}),nr=n({slug:"invalid-use-client",category:"BOUNDARY",status:400,title:"Invalid \'use client\' directive",suggestion:"Place \'use client\' at the top of the file"}),or=n({slug:"invalid-use-server",category:"BOUNDARY",status:400,title:"Invalid \'use server\' directive",suggestion:"Place \'use server\' at the top of the file or function"}),sr=n({slug:"rsc-payload-error",category:"BOUNDARY",status:500,title:"RSC payload serialization error",suggestion:"Ensure props are serializable (no functions, symbols, etc.)"}),ir=n({slug:"ssr-output-limit-exceeded",category:"BOUNDARY",status:500,title:"SSR output limit exceeded",suggestion:"Reduce the rendered HTML size or split the response into smaller pages"}),oe={"client-boundary-violation":er,"server-only-in-client":tr,"client-only-in-server":rr,"invalid-use-client":nr,"invalid-use-server":or,"rsc-payload-error":sr,"ssr-output-limit-exceeded":ir};var ar=n({slug:"hmr-error",category:"DEV",status:500,title:"Hot module replacement error",suggestion:"Restart the development server"}),cr=n({slug:"dev-server-error",category:"DEV",status:500,title:"Development server error",suggestion:"Check the dev server logs and restart"}),ur=n({slug:"fast-refresh-error",category:"DEV",status:500,title:"Fast refresh failed",suggestion:"Save the file again or restart the dev server"}),lr=n({slug:"error-overlay-error",category:"DEV",status:500,title:"Error overlay failed",suggestion:"Check browser console for details"}),gr=n({slug:"source-map-error",category:"DEV",status:500,title:"Source map loading error",suggestion:"Rebuild or clear cache"}),se={"hmr-error":ar,"dev-server-error":cr,"fast-refresh-error":ur,"error-overlay-error":lr,"source-map-error":gr};var dr=n({slug:"deployment-error",category:"DEPLOY",status:500,title:"Deployment process failed",suggestion:"Check deployment logs for details"}),fr=n({slug:"platform-error",category:"DEPLOY",status:500,title:"Platform-specific error",suggestion:"Check platform documentation and requirements"}),pr=n({slug:"env-var-missing",category:"DEPLOY",status:500,title:"Required environment variable missing",suggestion:"Set the required environment variable"}),Er=n({slug:"production-build-required",category:"DEPLOY",status:400,title:"Production build required",suggestion:"Run \'veryfront build\' before deploying"}),mr=n({slug:"environment-not-found",category:"DEPLOY",status:404,title:"Deployment environment not found",suggestion:"Check environment names with: veryfront config"}),Rr=n({slug:"release-missing-version",category:"DEPLOY",status:500,title:"Release has no version",suggestion:"Try again or check the build logs in Studio"}),yr=n({slug:"release-build-timeout",category:"DEPLOY",status:408,title:"Release build timed out",suggestion:"Try again or check the build logs in Studio"}),xr=n({slug:"deployment-verification-timeout",category:"DEPLOY",status:408,title:"Deployment verification timed out",suggestion:"Try again or check the deployment status in Studio"}),_r=n({slug:"push-receipt-missing",category:"DEPLOY",status:400,title:"Push receipt not found",suggestion:"Run: veryfront push --branch main first"}),hr=n({slug:"source-digest-mismatch",category:"DEPLOY",status:409,title:"Release source digest mismatch",suggestion:"Run veryfront push again to re-upload source files"}),Sr=n({slug:"preview-hostname-too-long",category:"DEPLOY",status:400,title:"Preview hostname too long",suggestion:"Use a shorter project slug or branch name"}),Ir=n({slug:"branch-not-found",category:"DEPLOY",status:404,title:"Branch not found",suggestion:"List branches in Studio or push a new one with: veryfront push --branch "}),ie={"deployment-error":dr,"platform-error":fr,"env-var-missing":pr,"production-build-required":Er,"environment-not-found":mr,"release-missing-version":Rr,"release-build-timeout":yr,"deployment-verification-timeout":xr,"push-receipt-missing":_r,"source-digest-mismatch":hr,"preview-hostname-too-long":Sr,"branch-not-found":Ir};var Or=n({slug:"agent-error",category:"AGENT",status:500,title:"Agent operation error",suggestion:"Check agent configuration and logs"}),Tr=n({slug:"agent-not-found",category:"AGENT",status:404,title:"Agent not found",suggestion:"Verify the agent ID exists"}),Cr=n({slug:"agent-timeout",category:"AGENT",status:408,title:"Agent operation timed out",suggestion:"Increase timeout or simplify the request"}),Ar=n({slug:"agent-intent-error",category:"AGENT",status:400,title:"Agent intent parsing error",suggestion:"Rephrase the request more clearly"}),Nr=n({slug:"orchestration-error",category:"AGENT",status:500,title:"Multi-agent orchestration error",suggestion:"Check agent coordination logic"}),Dr=n({slug:"cost-limit-exceeded",category:"AGENT",status:429,title:"Cost limit exceeded",suggestion:"Wait for the budget period to reset or increase the limit"}),br=n({slug:"tool-id-conflict",category:"AGENT",status:409,title:"Tool ID conflict",suggestion:"Use a unique tool ID or rename one of the conflicting tools"}),Lr=n({slug:"durable-run-event-persistence-failed",category:"AGENT",status:500,title:"Durable run event persistence failed",suggestion:"Correct invalid or oversized event data, or retry after durable event storage recovers"}),ae={"agent-error":Or,"agent-not-found":Tr,"agent-timeout":Cr,"agent-intent-error":Ar,"orchestration-error":Nr,"cost-limit-exceeded":Dr,"tool-id-conflict":br,"durable-run-event-persistence-failed":Lr};var Ur=n({slug:"unknown-error",category:"GENERAL",status:500,title:"Unknown/unclassified error",suggestion:"Check logs for more details"}),wr=n({slug:"authentication-required",category:"GENERAL",status:401,title:"Authentication required",suggestion:"Set VERYFRONT_API_TOKEN or run \'veryfront login\'"}),vr=n({slug:"permission-denied",category:"GENERAL",status:403,title:"File/resource permission denied",suggestion:"Check file permissions and access rights"}),Mr=n({slug:"file-not-found",category:"GENERAL",status:404,title:"File not found",suggestion:"Verify the file path exists"}),Pr=n({slug:"resource-not-found",category:"GENERAL",status:404,title:"Requested resource not found",suggestion:"Verify the referenced resource ID or name exists"}),$r=n({slug:"invalid-argument",category:"GENERAL",status:400,title:"Invalid function argument",suggestion:"Check argument types and values",exitCode:2}),kr=n({slug:"timeout-error",category:"GENERAL",status:408,title:"Operation timed out",suggestion:"Increase timeout or optimize the operation"}),Vr=n({slug:"initialization-error",category:"GENERAL",status:500,title:"Initialization failed",suggestion:"Check initialization requirements and dependencies"}),Gr=n({slug:"not-supported",category:"GENERAL",status:501,title:"Feature not supported",suggestion:"Check documentation for supported features"}),w=n({slug:"security-violation",category:"GENERAL",status:403,title:"Security violation detected",suggestion:"Check for path traversal or unauthorized access attempts"}),Fr=n({slug:"input-validation-failed",category:"GENERAL",status:400,title:"Input validation failed",suggestion:"Check request input against validation rules"}),Hr=n({slug:"project-source-empty",category:"GENERAL",status:400,title:"Project source is empty",suggestion:"Add project files or run \'veryfront init\'"}),ce={"unknown-error":Ur,"authentication-required":wr,"permission-denied":vr,"file-not-found":Mr,"resource-not-found":Pr,"invalid-argument":$r,"timeout-error":kr,"initialization-error":Vr,"not-supported":Gr,"security-violation":w,"input-validation-failed":Fr,"project-source-empty":Hr};var so=P(Z,Q,ee,te,re,ne,oe,se,ie,ae,ce);var jr=[{source:String.raw`]*>[\\s\\S]*?<\\/script>`,flags:"gi",name:"inline script"},{source:String.raw`javascript:`,flags:"gi",name:"javascript: URL"},{source:String.raw`\\bon\\w+\\s*=`,flags:"gi",name:"event handler attribute"},{source:String.raw`data:\\s*text\\/html`,flags:"gi",name:"data: HTML URL"}];function zr(){return jr.map(({source:t,flags:r,name:e})=>({pattern:new RegExp(t,r),name:e}))}function Yr(){let t=globalThis;return t.__VERYFRONT_DEV__===!0||t.Deno?.env?.get?.("VERYFRONT_ENV")==="development"}function ue(t,r={}){let{allowInlineScripts:e=!1,strict:o=!1,warn:s=!0}=r;for(let{pattern:i,name:a}of zr())if(!(e&&a==="inline script")&&(i.lastIndex=0,!!i.test(t)&&(s&&console.warn(`[Security] Suspicious ${a} detected in server HTML`),o||!Yr())))throw w.create({detail:`Potentially unsafe HTML: ${a} detected`});return t}var _=class{constructor(r,e){E(this,"prefix",r);E(this,"level",e)}log(r,e,o,...s){this.level>r||e?.(o,...s)}debug(r,...e){this.log(0,console.debug,`[${this.prefix}] DEBUG: ${r}`,...e)}info(r,...e){this.log(1,console.log,`[${this.prefix}] ${r}`,...e)}warn(r,...e){this.log(2,console.warn,`[${this.prefix}] WARN: ${r}`,...e)}error(r,...e){this.log(3,console.error,`[${this.prefix}] ERROR: ${r}`,...e)}};function Br(){if(typeof window>"u")return 2;let t=globalThis;return t.__VERYFRONT_DEV__||t.__RSC_DEV__?t.__VERYFRONT_DEBUG__||t.__RSC_DEBUG__?0:1:2}var C=Br(),R=new _("RSC",C),ho=new _("PREFETCH",C),So=new _("HYDRATE",C),Io=new _("VERYFRONT",C);var Co=Object.freeze({IPV4:"127.0.0.1",IPV6:"::1",HOSTNAME:"localhost"});var Wr=5e3,Kr=1e4,Do=16*1024*1024,qr=5e3;var Xr=100;var Jr=Object.freeze([5,10,25,50,75,100,250,500,750,1e3,2500,5e3,7500,1e4]),bo=Object.freeze([1,5,10,25,50,100,250,500,1e3,2500,5e3,1e4]),Lo=Object.freeze({server:Object.freeze({port:3e3,hostname:"0.0.0.0"}),timeouts:Object.freeze({default:Wr,api:3e4,ssr:Kr,hmr:3e4,sandbox:qr}),cache:Object.freeze({jit:Object.freeze({maxSize:Xr,tempDirPrefix:"vf-bundle-"})}),metrics:Object.freeze({ssrBoundaries:Jr})});var l="/_veryfront",v={RSC:`${l}/rsc/`,FS:`${l}/fs/`,MODULES:`${l}/modules/`,PAGES:`${l}/pages/`,DATA:`${l}/data/`,LIB:`${l}/lib/`,CHUNKS:`${l}/chunks/`,CLIENT:`${l}/client/`},ge={HMR_RUNTIME:`${l}/hmr-runtime.js`,HMR:`${l}/hmr.js`,ERROR_OVERLAY:`${l}/error-overlay.js`,DEV_LOADER:`${l}/dev-loader.js`,CLIENT_LOG:`${l}/log`,CLIENT_JS:`${l}/client.js`,ROUTER_JS:`${l}/router.js`,PREFETCH_JS:`${l}/prefetch.js`,MANIFEST_JSON:`${l}/manifest.json`,APP_JS:`${l}/app.js`,RSC_CLIENT:`${l}/rsc/client.js`,RSC_MANIFEST:`${l}/rsc/manifest`,RSC_STREAM:`${l}/rsc/stream`,RSC_PAYLOAD:`${l}/rsc/payload`,RSC_RENDER:`${l}/rsc/render`,RSC_PAGE:`${l}/rsc/page`,RSC_MODULE:`${l}/rsc/module`,RSC_DOM:`${l}/rsc/dom.js`,LIB_CHAT_REACT:`${l}/lib/chat/react.js`,LIB_CHAT_COMPONENTS:`${l}/lib/chat/components.js`,LIB_CHAT_PRIMITIVES:`${l}/lib/chat/primitives.js`};var Zr={ROOT:".veryfront",CACHE:".veryfront/cache",KV:".veryfront/kv",LOGS:".veryfront/logs",TMP:".veryfront/tmp"},wo=Zr.CACHE;var vo={HMR_RUNTIME:ge.HMR_RUNTIME,ERROR_OVERLAY:ge.ERROR_OVERLAY};var Qr=v.RSC,en=v.FS;var de="rsc-root",M="x-veryfront-dependency-pins";var qo=Object.freeze({react:"","react-dom":"","react-dom/client":"","react-dom/server":"","react/jsx-runtime":"","react/jsx-dev-runtime":""});var rn="veryfront-hydration-data";function fe(t){try{let r=[...t.querySelectorAll(`[id="${rn}"]`)];if(r.length!==1)return null;let e=t.body;if(!e)return null;let o=r[0];return e.firstElementChild!==o&&o.parentElement!==e||o.tagName?.toLowerCase()!=="script"||o.getAttribute("type")?.trim().toLowerCase()!=="application/json"?null:o}catch{return null}}function pe(t,r){if(!r?.startsWith("on:"))return!1;try{let e=fe(t);if(!e)return!1;let o=JSON.parse(e.textContent||"{}");return o.dependencyPinningCacheKey=r,e.textContent=JSON.stringify(o),!0}catch(e){return R.debug("hydration dependency snapshot seed failed",e),!1}}function me(t,r){let e=r==="root"?de:`rsc-slot-${r}`,o=t.getElementById(e);if(o)return o;let s=t.createElement("div");return s.id=e,t.body.appendChild(s),s}function nn(t,r){if(r.type!=="slot")return;let e=me(t,r.id);e.innerHTML=ue(String(r.html??""))}function Ee(t,r){let e=r.split(`\n`),o=e.pop()??"";for(let s of e){let i=s.trim();if(!i)continue;let a;try{a=JSON.parse(i)}catch(d){R.debug("[client-dom] malformed NDJSON line",{line:i,error:d instanceof Error?d.message:String(d)});continue}if(!a||typeof a!="object")continue;let c=a;if(c.type==="slot"){nn(t,c);try{an(t,c.id||"root")}catch(d){R.debug("[client-dom] hydration optional failed",d)}}}return o}function on(t){return new Promise((r,e)=>{let o=()=>e(new DOMException("aborted","AbortError"));if(t.aborted){o();return}t.addEventListener("abort",o,{once:!0})})}async function Es(t,r=document,e){let o="body"in t?t:null,s=o?.body??t;if(!s)return;o&&pe(r,o.headers.get(M));let i=s.getReader(),a=new TextDecoder,c="",d=!1;try{for(;;){if(e?.aborted)throw new DOMException("aborted","AbortError");let u=i.read(),{done:g,value:f}=e?await Promise.race([u,on(e)]):await u;if(g){d=!0;break}c+=a.decode(f,{stream:!0}),c=Ee(r,c)}c&&Ee(r,`${c}\n`)}catch(u){throw u instanceof Error&&u.name==="AbortError"||R.debug("[client-dom] consumeNdjsonStream error",u),u}finally{try{await i.cancel()}catch(u){d||R.debug("[client-dom] reader.cancel failed",u)}try{i.releaseLock()}catch(u){R.debug("[client-dom] reader.releaseLock failed",u)}if(typeof s.cancel=="function")try{await s.cancel()}catch(u){R.debug("[client-dom] stream.cancel failed",u)}if(typeof o?.body?.cancel=="function")try{await o.body.cancel()}catch(u){R.debug("[client-dom] response.body.cancel failed",u)}}}function sn(t,r){let e=me(t,r),o=[],s=i=>{let a=i;a.dataset?.clientRef&&o.push(a);for(let c of i.children)s(c)};return s(e),o}function an(t,r){let e=sn(t,r);for(let o of e){let s=o.dataset?.clientRef;s&&(o.dataset.hydrated="true",R.debug("[client-dom] marked for hydration",s))}}export{Es as consumeNdjsonStream,me as getContainer};\n'; + 'var xe=Object.defineProperty;var he=(t,r,e)=>r in t?xe(t,r,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[r]=e;var E=(t,r,e)=>he(t,typeof r!="symbol"?r+"":r,e);var _e=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function Se(t,...r){let e=Object.create(null),o=t.charAt(0).toUpperCase()+t.slice(1);for(let s of r)for(let[i,a]of Object.entries(s)){if(typeof a!="object"||a===null||!Object.hasOwn(a,"slug"))throw new Error(`${o} entry "${i}" must define a slug`);let c=a.slug;if(typeof c!="string")throw new Error(`${o} entry "${i}" must define a string slug`);if(c!==i)throw new Error(`${o} key "${i}" does not match entry slug "${c}"`);if(Object.hasOwn(e,i))throw new Error(`Duplicate ${t} slug "${i}"`);e[i]=a}return Object.freeze(e)}function M(...t){for(let r of t)for(let e of Object.values(r)){if(typeof e.slug!="string"||e.slug.length<3||e.slug.length>40||!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(e.slug))throw new TypeError(`Registered error slug must be 3-40 characters of lowercase kebab-case, got "${e.slug}"`);if(typeof e.category!="string"||!_e.has(e.category))throw new TypeError(`Registered error has unknown category "${e.category}"`);if(!Number.isInteger(e.status)||e.status<400||e.status>=600)throw new RangeError(`Registered error status must be an integer from 400 through 599, got ${e.status}`);if(typeof e.title!="string"||e.title.trim().length===0)throw new TypeError("Registered error title must be a non-empty string");if(e.suggestion!==void 0&&(typeof e.suggestion!="string"||e.suggestion.trim().length===0))throw new TypeError("Registered error suggestion must be non-empty when provided")}return Se("error registry",...t)}var T={reset:"\\x1B[0m",dim:"\\x1B[2m",gray:"\\x1B[90m",red:"\\x1B[31m",green:"\\x1B[32m",yellow:"\\x1B[33m",blue:"\\x1B[34m",magenta:"\\x1B[35m",cyan:"\\x1B[36m"},ln={debug:T.gray,info:T.green,warn:T.yellow,error:T.red};var p="[REDACTED]",m=Reflect.apply;var $=RegExp.prototype.exec,y=RegExp.prototype[Symbol.replace],dn=String.prototype.charCodeAt,k=String.prototype.slice,Te=String.prototype.toLowerCase,Ie=/[^a-z0-9]/g;function b(t){let r=m(Te,t,[]);return m(y,Ie,[r,""])}function I(t,r,e){return e===void 0?m(k,t,[r]):m(k,t,[r,e])}var Oe=["password","passwd","pwd","passphrase","secret","clientsecret","token","apikey","accesskey","privatekey","credential","authorization","cookie","bearer","jwt","connectionstring","signature","sessionid","sid","otp","mfa","pin","salt","xsrf","csrf"],Ae=512,Ce=128,S=new Map;function F(t){let r=t.length<=Ce;if(r){let s=S.get(t);if(s!==void 0)return s}let e=b(t),o=Oe.some(s=>e.includes(s));if(r){if(S.size>=Ae){let s=S.keys().next().value;s!==void 0&&S.delete(s)}S.set(t,o)}return o}var Ne=["access_token","accesstoken","refresh_token","api_key","apikey","code","token","secret","client_secret","password","passwd","pwd","state","sig","signature","auth","x-amz-credential","x-amz-signature","x-amz-security-token","x-goog-credential","x-goog-signature"],be=new Set(Ne.map(b)),De=/(\\b[a-z][a-z0-9+.-]*:\\/\\/|\\/\\/)([^/?#\\s]+)@/gi,Le=/(\\b[a-z][a-z0-9+.-]*:\\/\\/|\\/\\/)([a-z0-9._~!$&\'()*+,;=%-]+):([^/?#@\\r\\n \\t]+[ \\t][^/?#@\\r\\n]*)@/gi,Ue=3;function ve(t){return t===" "||t==="\t"||t===","||t===";"||t==="&"||t==="?"||t==="#"}function we(t){if(!t)return!1;let r=t.charCodeAt(0);return r>=65&&r<=90||r>=97&&r<=122}function H(t){return we(t)||t==="_"||t==="$"}function Pe(t){if(!t)return!1;let r=t.charCodeAt(0);return H(t)||r>=48&&r<=57||t==="."||t==="-"}function j(t,r){let e=r,o=t[e]===\'"\'||t[e]==="\'"?t[e++]:"";if(!H(t[e]))return!1;for(e++;Pe(t[e]);)e++;if(o){if(t[e]!==o)return!1;e++}for(;t[e]===" "||t[e]==="\t";)e++;return t[e]===":"||t[e]==="="}function z(t){return t==="\\r"||t===`\n`||t==="}"||t==="]"||ve(t)}function Y(t,r){let e=r;for(;e=t.length||j(t,e)}function Me(t,r){let e=r,o=!0;if(t.startsWith(p,r)){let g=r+p.length;if(V(t,g))return{end:g,replacement:p};e=g,o=!1}let s=o&&(t[e]===\'"\'||t[e]==="\'"||t[e]==="`")?t[e]:"",i=!1,a=()=>s?`${s}${p}${i?s:""}`:p,c=[],d="",u=-1;for(let g=e;g0&&(f==="}"||f==="]")){if(c.at(-1)!==f)return{end:t.length,replacement:a()};if(c.pop(),g++,c.length===0&&V(t,g))return{end:g,replacement:a()};continue}if(c.length>0||!z(f)){g++;continue}let _=g;if(g=Y(t,g),g>=t.length||j(t,g))return{end:_,replacement:a()}}return{end:t.length,replacement:a()}}function G(t,r,e,o){let s=0,i="";for(let a=m($,r,[t]);a;a=m($,r,[t])){let c=a[e];if(!F(c))continue;let d=r.lastIndex,u=o===void 0?void 0:a[o],g=d+p.length;if((u==="?"||u==="&"||u===";")&&t.startsWith(p,d)&&t[g]==="#")continue;let f=Me(t,d);i+=I(t,s,a.index),i+=a[0],i+=f.replacement,s=f.end,r.lastIndex=f.end}return s===0?t:i+I(t,s)}function $e(t,r,e){let o=e.search(/[ \\t]/);if(o<0)return!1;let s=`${r}:${I(e,0,o)}`,i=t==="//"?`https://${s}`:`${t}${s}`;try{let a=new URL(i);return a.username.length===0&&a.password.length===0}catch{return!1}}function ke(t){let r=t;for(let e=0;e{let i=s.indexOf(":");if(i===-1)return`${o}${p}@`;let a=I(s,0,i);return`${o}${a}:${p}@`}]);return r=m(y,Le,[r,(e,o,s,i)=>$e(o,s,i)?e:`${o}${s}:${p}@`]),r=m(y,/([?#&;])([-a-z0-9_.%\\[\\]]+)=([^&#;\\s]*)/gi,[r,(e,o,s,i)=>{let a=ke(s);return be.has(b(a))||F(a)?`${o}${s}=${p}`:e}]),r=m(y,/(^|[^a-z0-9_-])((?:set-cookie|cookie)\\s*:\\s*)[^\\r\\n]*/gi,[r,(e,o,s)=>`${o}${s}${p}`]),r=m(y,/\\b(authorization\\s*[:=]\\s*)[^\\r\\n]*/gi,[r,(e,o)=>`${o}${p}`]),r=m(y,/\\b(bearer|basic)(\\s+)(?:"[^"\\r\\n]*"|\'[^\'\\r\\n]*\'|[a-z0-9._~+/=-]+)/gi,[r,(e,o,s)=>`${o}${s}${p}`]),r=G(r,/(["\'])([_$a-z][a-z0-9_.$-]*)\\1(\\s*[:=]\\s*)/gi,2),r=G(r,/(^|[^a-z0-9_.$-])([_$a-z][a-z0-9_.$-]*)(\\s*[:=]\\s*)/gi,2,1),r}var Ve=2048;var Rn=64*1024,Ge=256,Fe="https://veryfront.com/docs/errors/",B="...[truncated]",L="unknown-error";function W(t,r){if(t.length<=r)return t;let e=Math.max(0,r-B.length);return`${He(t,e)}${B}`}function He(t,r){let e=t.slice(0,r),o=e.charCodeAt(e.length-1);return o>=55296&&o<=56319&&(e=e.slice(0,-1)),e}function je(t){let r="";for(let e=0;e=55296&&o<=56319){let s=t.charCodeAt(e+1);s>=56320&&s<=57343?(r+=t.slice(e,e+2),e++):r+="\\uFFFD";continue}r+=o>=56320&&o<=57343?"\\uFFFD":t.charAt(e)}return r}function x(t){return typeof t!="string"?p:W(D(t),Ve)}function ze(t){let r=typeof t=="string"?D(t):L,e=W(r||L,Ge),o=je(e);return o==="."||o===".."?L:o}function O(t){let r=encodeURIComponent(ze(t));return`${Fe}${r}`}var Ye=Object.freeze,Be=Object.getOwnPropertyDescriptors,K=Number.isFinite,X=new WeakSet,We=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function n(t){let r={...t},e={...r,create(o){let s=o?.message,i=o?.detail,a=o?.cause,c=o?.instance,d=o?.context,u=o?.status??r.status;return new U(s||i||r.title,{slug:r.slug,category:r.category,status:u,title:r.title,suggestion:r.suggestion,exitCode:r.exitCode,detail:i,cause:a,instance:c,context:d})}};return Ye(e)}var U=class extends Error{constructor(e,o){super(e);E(this,"slug");E(this,"category");E(this,"status");E(this,"title");E(this,"suggestion");E(this,"exitCode");E(this,"detail");E(this,"cause");E(this,"instance");E(this,"context");X.add(this),this.name="VeryfrontError",this.slug=o.slug,this.category=o.category,this.status=o.status,this.title=o.title,this.suggestion=o.suggestion,this.exitCode=o.exitCode,this.detail=o.detail,this.cause=o.cause,this.instance=o.instance,this.context=o.context}toRFC9457(){let e=q(this);return e?{type:O(e.slug),title:x(e.title),status:e.status,detail:e.detail===void 0?void 0:x(e.detail),instance:e.instance===void 0?void 0:x(e.instance),category:e.category,suggestion:e.suggestion===void 0?void 0:x(e.suggestion),cause:typeof e.cause=="string"?x(e.cause):void 0}:{type:O("unknown-error"),title:"Unknown/unclassified error",status:500,category:"GENERAL"}}getDocsUrl(){let e=q(this);return O(e?.slug??"unknown-error")}};function J(t){return typeof t=="object"&&t!==null&&X.has(t)}function q(t){return J(t)?Ke(t):null}function Ke(t){try{if(!J(t))return null;let r=Be(t),e=ye=>{let N=r[ye];return N&&"value"in N?N.value:void 0},o=e("slug"),s=e("category"),i=e("status"),a=e("title"),c=e("message"),d=e("suggestion"),u=e("exitCode"),g=e("detail"),f=e("cause"),_=e("instance"),Re=e("context"),C=e("stack");return typeof o!="string"||!We.has(s)||typeof i!="number"||!K(i)||typeof a!="string"||typeof c!="string"||d!==void 0&&typeof d!="string"||u!==void 0&&(typeof u!="number"||!K(u))||g!==void 0&&typeof g!="string"||_!==void 0&&typeof _!="string"||C!==void 0&&typeof C!="string"?null:{slug:o,category:s,status:i,title:a,message:c,suggestion:d,exitCode:u,detail:g,cause:f,instance:_,context:Re,stack:C}}catch{return null}}var qe=n({slug:"config-not-found",category:"CONFIG",status:404,title:"Configuration file not found",suggestion:"Create veryfront.config.js, veryfront.config.ts, or veryfront.config.mjs in the project root"}),Xe=n({slug:"config-invalid",category:"CONFIG",status:400,title:"Invalid configuration format",suggestion:"Check the reported configuration path and validation details"}),Je=n({slug:"config-parse-error",category:"CONFIG",status:400,title:"Failed to parse configuration",suggestion:"Ensure your configuration file contains valid JavaScript or TypeScript"}),Ze=n({slug:"config-validation-error",category:"CONFIG",status:422,title:"Configuration validation failed",suggestion:"Check the configuration against the schema requirements"}),Qe=n({slug:"config-type-error",category:"CONFIG",status:400,title:"Configuration type mismatch",suggestion:"Ensure configuration values match expected types"}),et=n({slug:"import-map-invalid",category:"CONFIG",status:400,title:"Invalid import map configuration",suggestion:"Check your import map syntax and paths"}),tt=n({slug:"cors-config-invalid",category:"CONFIG",status:400,title:"Invalid CORS configuration",suggestion:"Review CORS settings in your configuration"}),rt=n({slug:"config-validation-failed",category:"CONFIG",status:400,title:"Configuration validation failed",suggestion:"Check configuration values against requirements"}),nt=n({slug:"webhook-config-invalid",category:"CONFIG",status:400,title:"Invalid webhook configuration",suggestion:"Check webhook definition fields, target settings, and eventFilter conditions"}),ot=n({slug:"schedule-config-invalid",category:"CONFIG",status:400,title:"Invalid schedule configuration",suggestion:"Check schedule definition fields, cron expression, target settings, and positive-integer limits"}),st=n({slug:"trigger-config-invalid",category:"CONFIG",status:400,title:"Invalid trigger configuration",suggestion:"Check trigger ID format (lowercase, alphanumeric, dots/slashes/hyphens) and ensure all input values are JSON-serializable"}),Z={"config-not-found":qe,"config-invalid":Xe,"config-parse-error":Je,"config-validation-error":Ze,"config-type-error":Qe,"import-map-invalid":et,"cors-config-invalid":tt,"config-validation-failed":rt,"webhook-config-invalid":nt,"schedule-config-invalid":ot,"trigger-config-invalid":st};var it=n({slug:"build-failed",category:"BUILD",status:500,title:"Build process failed",suggestion:"Check the build output for specific errors"}),at=n({slug:"bundle-error",category:"BUILD",status:500,title:"Bundle generation failed",suggestion:"Review bundler output for details"}),ct=n({slug:"typescript-error",category:"BUILD",status:500,title:"TypeScript compilation error",suggestion:"Fix TypeScript errors shown in the output"}),ut=n({slug:"mdx-compile-error",category:"BUILD",status:500,title:"MDX compilation failed",suggestion:"Check your MDX file syntax"}),lt=n({slug:"asset-optimization-error",category:"BUILD",status:500,title:"Asset optimization failed",suggestion:"Check asset file formats and paths"}),gt=n({slug:"ssg-generation-error",category:"BUILD",status:500,title:"Static site generation failed",suggestion:"Review SSG configuration and data fetching"}),dt=n({slug:"sourcemap-error",category:"BUILD",status:500,title:"Source map generation failed",suggestion:"Check source map configuration"}),ft=n({slug:"compilation-error",category:"BUILD",status:500,title:"Compilation failed",suggestion:"Review compiler output for specific errors"}),Q={"build-failed":it,"bundle-error":at,"typescript-error":ct,"mdx-compile-error":ut,"asset-optimization-error":lt,"ssg-generation-error":gt,"sourcemap-error":dt,"compilation-error":ft};var pt=n({slug:"hydration-mismatch",category:"RUNTIME",status:500,title:"Client/server hydration mismatch",suggestion:"Ensure server and client render the same content"}),Et=n({slug:"render-error",category:"RUNTIME",status:500,title:"Component render failed",suggestion:"Check component for runtime errors"}),mt=n({slug:"component-error",category:"RUNTIME",status:500,title:"Component execution error",suggestion:"Review component logic and props"}),Rt=n({slug:"layout-not-found",category:"RUNTIME",status:404,title:"Layout component not found",suggestion:"Ensure layout file exists at the expected path"}),yt=n({slug:"page-not-found",category:"RUNTIME",status:404,title:"Page component not found",suggestion:"Check that the page file exists in the routes directory"}),xt=n({slug:"api-error",category:"RUNTIME",status:500,title:"API route handler error",suggestion:"Review API route handler for errors"}),ht=n({slug:"middleware-error",category:"RUNTIME",status:500,title:"Middleware execution error",suggestion:"Check middleware function for errors"}),_t=n({slug:"trigger-target-not-found",category:"RUNTIME",status:404,title:"Trigger target not found",suggestion:"Ensure the referenced task or workflow ID is registered in the project"}),St=n({slug:"trigger-execution-failed",category:"RUNTIME",status:500,title:"Trigger target execution failed",suggestion:"Check the task or workflow for errors and review the trigger input"}),Tt=n({slug:"trigger-not-supported",category:"RUNTIME",status:501,title:"Trigger target type not supported in local runtime",suggestion:"Use a workflow or task target for local trigger runs; agent targets require the Cloud runtime"}),ee={"hydration-mismatch":pt,"render-error":Et,"component-error":mt,"layout-not-found":Rt,"page-not-found":yt,"api-error":xt,"middleware-error":ht,"trigger-target-not-found":_t,"trigger-execution-failed":St,"trigger-not-supported":Tt};var It=n({slug:"route-conflict",category:"ROUTE",status:409,title:"Conflicting route definitions",suggestion:"Rename or reorganize conflicting route files"}),Ot=n({slug:"invalid-route-file",category:"ROUTE",status:400,title:"Invalid route file structure",suggestion:"Ensure route file exports required functions"}),At=n({slug:"route-handler-invalid",category:"ROUTE",status:400,title:"Invalid route handler export",suggestion:"Export a valid handler function from the route file"}),Ct=n({slug:"dynamic-route-error",category:"ROUTE",status:500,title:"Dynamic route parsing failed",suggestion:"Check dynamic route segment syntax"}),Nt=n({slug:"route-params-error",category:"ROUTE",status:400,title:"Route parameters invalid",suggestion:"Validate route parameter values"}),bt=n({slug:"api-route-error",category:"ROUTE",status:500,title:"API route definition error",suggestion:"Review API route configuration"}),te={"route-conflict":It,"invalid-route-file":Ot,"route-handler-invalid":At,"dynamic-route-error":Ct,"route-params-error":Nt,"api-route-error":bt};var Dt=n({slug:"module-not-found",category:"MODULE",status:404,title:"Module could not be resolved",suggestion:"Check the import path and ensure the module is installed"}),Lt=n({slug:"import-resolution-error",category:"MODULE",status:500,title:"Import path resolution failed",suggestion:"Verify import paths and module configuration"}),Ut=n({slug:"circular-dependency",category:"MODULE",status:500,title:"Circular dependency detected",suggestion:"Refactor imports to break the circular dependency"}),vt=n({slug:"invalid-import",category:"MODULE",status:400,title:"Invalid import statement",suggestion:"Fix import syntax or path"}),wt=n({slug:"dependency-missing",category:"MODULE",status:404,title:"Required dependency not installed",suggestion:"Install the missing dependency with your package manager"}),Pt=n({slug:"version-mismatch",category:"MODULE",status:409,title:"Dependency version mismatch",suggestion:"Update dependencies to compatible versions"}),re={"module-not-found":Dt,"import-resolution-error":Lt,"circular-dependency":Ut,"invalid-import":vt,"dependency-missing":wt,"version-mismatch":Pt};var Mt=n({slug:"port-in-use",category:"SERVER",status:409,title:"Server port already in use",suggestion:"Use a different port or stop the process using this port"}),$t=n({slug:"server-start-error",category:"SERVER",status:500,title:"Server failed to start",suggestion:"Check server configuration and port availability"}),kt=n({slug:"cache-error",category:"SERVER",status:500,title:"Cache operation failed",suggestion:"Clear the cache and try again"}),Vt=n({slug:"file-watch-error",category:"SERVER",status:500,title:"File watcher error",suggestion:"Restart the development server"}),Gt=n({slug:"request-error",category:"SERVER",status:500,title:"HTTP request handling error",suggestion:"Check request handler and middleware"}),Ft=n({slug:"service-overloaded",category:"SERVER",status:503,title:"Service overloaded",suggestion:"Reduce load or scale up resources"}),Ht=n({slug:"project-execution-unavailable",category:"SERVER",status:503,title:"Project execution unavailable",suggestion:"Route the project to a dedicated isolated runtime"}),jt=n({slug:"semaphore-timeout",category:"SERVER",status:503,title:"Semaphore acquire timeout",suggestion:"Reduce concurrency or increase the semaphore acquire timeout"}),zt=n({slug:"circuit-breaker-open",category:"SERVER",status:503,title:"Circuit breaker is open",suggestion:"Wait for the breaker reset timeout before retrying"}),Yt=n({slug:"cache-path-mismatch",category:"SERVER",status:500,title:"Cache path mismatch",suggestion:"Clear the cache directory and rebuild"}),Bt=n({slug:"network-error",category:"SERVER",status:502,title:"Network operation failed",suggestion:"Check network connectivity and retry"}),Wt=n({slug:"api-client-error",category:"SERVER",status:500,title:"API client request failed",suggestion:"Check API connectivity and authentication"}),Kt=n({slug:"token-storage-error",category:"SERVER",status:500,title:"Token storage operation failed",suggestion:"Check token storage backend and credentials"}),qt=n({slug:"cache-invariant-violation",category:"SERVER",status:500,title:"Cache path invariant violated",suggestion:"Clear the cache and rebuild"}),Xt=n({slug:"release-not-found",category:"SERVER",status:404,title:"No active release found",suggestion:"Deploy the project to create a release for this environment"}),Jt=n({slug:"fallback-exhausted",category:"SERVER",status:500,title:"Primary and fallback operations both failed",suggestion:"Check service availability and connectivity"}),Zt=n({slug:"rag-store-corrupt",category:"SERVER",status:500,title:"RAG store file is corrupt",suggestion:"Repair or move the store file aside, then retry; it was not overwritten"}),Qt=n({slug:"rag-store-unavailable",category:"SERVER",status:500,title:"RAG store file is unavailable",suggestion:"Check storage availability, permissions, and concurrent operations, then retry"}),ne={"port-in-use":Mt,"server-start-error":$t,"cache-error":kt,"file-watch-error":Vt,"request-error":Gt,"service-overloaded":Ft,"project-execution-unavailable":Ht,"semaphore-timeout":jt,"circuit-breaker-open":zt,"cache-path-mismatch":Yt,"network-error":Bt,"api-client-error":Wt,"token-storage-error":Kt,"cache-invariant-violation":qt,"release-not-found":Xt,"fallback-exhausted":Jt,"rag-store-corrupt":Zt,"rag-store-unavailable":Qt};var er=n({slug:"client-boundary-violation",category:"BOUNDARY",status:400,title:"Client boundary rule violation",suggestion:"Add \'use client\' directive or move code to a client component"}),tr=n({slug:"server-only-in-client",category:"BOUNDARY",status:400,title:"Server-only code in client component",suggestion:"Move server-only code to a server component"}),rr=n({slug:"client-only-in-server",category:"BOUNDARY",status:400,title:"Client-only code in server component",suggestion:"Move client-only code to a client component"}),nr=n({slug:"invalid-use-client",category:"BOUNDARY",status:400,title:"Invalid \'use client\' directive",suggestion:"Place \'use client\' at the top of the file"}),or=n({slug:"invalid-use-server",category:"BOUNDARY",status:400,title:"Invalid \'use server\' directive",suggestion:"Place \'use server\' at the top of the file or function"}),sr=n({slug:"rsc-payload-error",category:"BOUNDARY",status:500,title:"RSC payload serialization error",suggestion:"Ensure props are serializable (no functions, symbols, etc.)"}),ir=n({slug:"ssr-output-limit-exceeded",category:"BOUNDARY",status:500,title:"SSR output limit exceeded",suggestion:"Reduce the rendered HTML size or split the response into smaller pages"}),oe={"client-boundary-violation":er,"server-only-in-client":tr,"client-only-in-server":rr,"invalid-use-client":nr,"invalid-use-server":or,"rsc-payload-error":sr,"ssr-output-limit-exceeded":ir};var ar=n({slug:"hmr-error",category:"DEV",status:500,title:"Hot module replacement error",suggestion:"Restart the development server"}),cr=n({slug:"dev-server-error",category:"DEV",status:500,title:"Development server error",suggestion:"Check the dev server logs and restart"}),ur=n({slug:"fast-refresh-error",category:"DEV",status:500,title:"Fast refresh failed",suggestion:"Save the file again or restart the dev server"}),lr=n({slug:"error-overlay-error",category:"DEV",status:500,title:"Error overlay failed",suggestion:"Check browser console for details"}),gr=n({slug:"source-map-error",category:"DEV",status:500,title:"Source map loading error",suggestion:"Rebuild or clear cache"}),se={"hmr-error":ar,"dev-server-error":cr,"fast-refresh-error":ur,"error-overlay-error":lr,"source-map-error":gr};var dr=n({slug:"deployment-error",category:"DEPLOY",status:500,title:"Deployment process failed",suggestion:"Check deployment logs for details"}),fr=n({slug:"platform-error",category:"DEPLOY",status:500,title:"Platform-specific error",suggestion:"Check platform documentation and requirements"}),pr=n({slug:"env-var-missing",category:"DEPLOY",status:500,title:"Required environment variable missing",suggestion:"Set the required environment variable"}),Er=n({slug:"production-build-required",category:"DEPLOY",status:400,title:"Production build required",suggestion:"Run \'veryfront build\' before deploying"}),mr=n({slug:"environment-not-found",category:"DEPLOY",status:404,title:"Deployment environment not found",suggestion:"Check environment names with: veryfront config"}),Rr=n({slug:"release-missing-version",category:"DEPLOY",status:500,title:"Release has no version",suggestion:"Try again or check the build logs in Studio"}),yr=n({slug:"release-build-timeout",category:"DEPLOY",status:408,title:"Release build timed out",suggestion:"Try again or check the build logs in Studio"}),xr=n({slug:"deployment-verification-timeout",category:"DEPLOY",status:408,title:"Deployment verification timed out",suggestion:"Try again or check the deployment status in Studio"}),hr=n({slug:"push-receipt-missing",category:"DEPLOY",status:400,title:"Push receipt not found",suggestion:"Run: veryfront push --branch main first"}),_r=n({slug:"source-digest-mismatch",category:"DEPLOY",status:409,title:"Release source digest mismatch",suggestion:"Run veryfront push again to re-upload source files"}),Sr=n({slug:"preview-hostname-too-long",category:"DEPLOY",status:400,title:"Preview hostname too long",suggestion:"Use a shorter project slug or branch name"}),Tr=n({slug:"branch-not-found",category:"DEPLOY",status:404,title:"Branch not found",suggestion:"List branches in Studio or push a new one with: veryfront push --branch "}),ie={"deployment-error":dr,"platform-error":fr,"env-var-missing":pr,"production-build-required":Er,"environment-not-found":mr,"release-missing-version":Rr,"release-build-timeout":yr,"deployment-verification-timeout":xr,"push-receipt-missing":hr,"source-digest-mismatch":_r,"preview-hostname-too-long":Sr,"branch-not-found":Tr};var Ir=n({slug:"agent-error",category:"AGENT",status:500,title:"Agent operation error",suggestion:"Check agent configuration and logs"}),Or=n({slug:"agent-not-found",category:"AGENT",status:404,title:"Agent not found",suggestion:"Verify the agent ID exists"}),Ar=n({slug:"agent-timeout",category:"AGENT",status:408,title:"Agent operation timed out",suggestion:"Increase timeout or simplify the request"}),Cr=n({slug:"agent-intent-error",category:"AGENT",status:400,title:"Agent intent parsing error",suggestion:"Rephrase the request more clearly"}),Nr=n({slug:"orchestration-error",category:"AGENT",status:500,title:"Multi-agent orchestration error",suggestion:"Check agent coordination logic"}),br=n({slug:"cost-limit-exceeded",category:"AGENT",status:429,title:"Cost limit exceeded",suggestion:"Wait for the budget period to reset or increase the limit"}),Dr=n({slug:"tool-id-conflict",category:"AGENT",status:409,title:"Tool ID conflict",suggestion:"Use a unique tool ID or rename one of the conflicting tools"}),Lr=n({slug:"durable-run-event-persistence-failed",category:"AGENT",status:500,title:"Durable run event persistence failed",suggestion:"Correct invalid or oversized event data, or retry after durable event storage recovers"}),ae={"agent-error":Ir,"agent-not-found":Or,"agent-timeout":Ar,"agent-intent-error":Cr,"orchestration-error":Nr,"cost-limit-exceeded":br,"tool-id-conflict":Dr,"durable-run-event-persistence-failed":Lr};var Ur=n({slug:"unknown-error",category:"GENERAL",status:500,title:"Unknown/unclassified error",suggestion:"Check logs for more details"}),vr=n({slug:"authentication-required",category:"GENERAL",status:401,title:"Authentication required",suggestion:"Set VERYFRONT_API_TOKEN or run \'veryfront login\'"}),wr=n({slug:"permission-denied",category:"GENERAL",status:403,title:"File/resource permission denied",suggestion:"Check file permissions and access rights"}),Pr=n({slug:"file-not-found",category:"GENERAL",status:404,title:"File not found",suggestion:"Verify the file path exists"}),Mr=n({slug:"resource-not-found",category:"GENERAL",status:404,title:"Requested resource not found",suggestion:"Verify the referenced resource ID or name exists"}),$r=n({slug:"invalid-argument",category:"GENERAL",status:400,title:"Invalid function argument",suggestion:"Check argument types and values",exitCode:2}),kr=n({slug:"timeout-error",category:"GENERAL",status:408,title:"Operation timed out",suggestion:"Increase timeout or optimize the operation"}),Vr=n({slug:"initialization-error",category:"GENERAL",status:500,title:"Initialization failed",suggestion:"Check initialization requirements and dependencies"}),Gr=n({slug:"not-supported",category:"GENERAL",status:501,title:"Feature not supported",suggestion:"Check documentation for supported features"}),v=n({slug:"security-violation",category:"GENERAL",status:403,title:"Security violation detected",suggestion:"Check for path traversal or unauthorized access attempts"}),Fr=n({slug:"input-validation-failed",category:"GENERAL",status:400,title:"Input validation failed",suggestion:"Check request input against validation rules"}),Hr=n({slug:"project-source-empty",category:"GENERAL",status:400,title:"Project source is empty",suggestion:"Add project files or run \'veryfront init\'"}),ce={"unknown-error":Ur,"authentication-required":vr,"permission-denied":wr,"file-not-found":Pr,"resource-not-found":Mr,"invalid-argument":$r,"timeout-error":kr,"initialization-error":Vr,"not-supported":Gr,"security-violation":v,"input-validation-failed":Fr,"project-source-empty":Hr};var so=M(Z,Q,ee,te,re,ne,oe,se,ie,ae,ce);var jr=[{source:String.raw`]*>[\\s\\S]*?<\\/script>`,flags:"gi",name:"inline script"},{source:String.raw`javascript:`,flags:"gi",name:"javascript: URL"},{source:String.raw`\\bon\\w+\\s*=`,flags:"gi",name:"event handler attribute"},{source:String.raw`data:\\s*text\\/html`,flags:"gi",name:"data: HTML URL"}];function zr(){return jr.map(({source:t,flags:r,name:e})=>({pattern:new RegExp(t,r),name:e}))}function Yr(){let t=globalThis;return t.__VERYFRONT_DEV__===!0||t.Deno?.env?.get?.("VERYFRONT_ENV")==="development"}function ue(t,r={}){let{allowInlineScripts:e=!1,strict:o=!1,warn:s=!0}=r;for(let{pattern:i,name:a}of zr())if(!(e&&a==="inline script")&&(i.lastIndex=0,!!i.test(t)&&(s&&console.warn(`[Security] Suspicious ${a} detected in server HTML`),o||!Yr())))throw v.create({detail:`Potentially unsafe HTML: ${a} detected`});return t}var h=class{constructor(r,e){E(this,"prefix",r);E(this,"level",e)}log(r,e,o,...s){this.level>r||e?.(o,...s)}debug(r,...e){this.log(0,console.debug,`[${this.prefix}] DEBUG: ${r}`,...e)}info(r,...e){this.log(1,console.log,`[${this.prefix}] ${r}`,...e)}warn(r,...e){this.log(2,console.warn,`[${this.prefix}] WARN: ${r}`,...e)}error(r,...e){this.log(3,console.error,`[${this.prefix}] ERROR: ${r}`,...e)}};function Br(){if(typeof window>"u")return 2;let t=globalThis;return t.__VERYFRONT_DEV__||t.__RSC_DEV__?t.__VERYFRONT_DEBUG__||t.__RSC_DEBUG__?0:1:2}var A=Br(),R=new h("RSC",A),_o=new h("PREFETCH",A),So=new h("HYDRATE",A),To=new h("VERYFRONT",A);var Ao=Object.freeze({IPV4:"127.0.0.1",IPV6:"::1",HOSTNAME:"localhost"});var Wr=5e3,Kr=1e4,bo=16*1024*1024,qr=5e3;var Xr=100;var Jr=Object.freeze([5,10,25,50,75,100,250,500,750,1e3,2500,5e3,7500,1e4]),Do=Object.freeze([1,5,10,25,50,100,250,500,1e3,2500,5e3,1e4]),Lo=Object.freeze({server:Object.freeze({port:3e3,hostname:"0.0.0.0"}),timeouts:Object.freeze({default:Wr,api:3e4,ssr:Kr,hmr:3e4,sandbox:qr}),cache:Object.freeze({jit:Object.freeze({maxSize:Xr,tempDirPrefix:"vf-bundle-"})}),metrics:Object.freeze({ssrBoundaries:Jr})});var l="/_veryfront",w={RSC:`${l}/rsc/`,FS:`${l}/fs/`,MODULES:`${l}/modules/`,PAGES:`${l}/pages/`,DATA:`${l}/data/`,LIB:`${l}/lib/`,CHUNKS:`${l}/chunks/`,CLIENT:`${l}/client/`},ge={HMR_RUNTIME:`${l}/hmr-runtime.js`,HMR:`${l}/hmr.js`,ERROR_OVERLAY:`${l}/error-overlay.js`,DEV_LOADER:`${l}/dev-loader.js`,CLIENT_LOG:`${l}/log`,CLIENT_JS:`${l}/client.js`,ROUTER_JS:`${l}/router.js`,PREFETCH_JS:`${l}/prefetch.js`,MANIFEST_JSON:`${l}/manifest.json`,APP_JS:`${l}/app.js`,RSC_CLIENT:`${l}/rsc/client.js`,RSC_MANIFEST:`${l}/rsc/manifest`,RSC_STREAM:`${l}/rsc/stream`,RSC_PAYLOAD:`${l}/rsc/payload`,RSC_RENDER:`${l}/rsc/render`,RSC_PAGE:`${l}/rsc/page`,RSC_MODULE:`${l}/rsc/module`,RSC_DOM:`${l}/rsc/dom.js`,LIB_CHAT_REACT:`${l}/lib/chat/react.js`,LIB_CHAT_COMPONENTS:`${l}/lib/chat/components.js`,LIB_CHAT_PRIMITIVES:`${l}/lib/chat/primitives.js`};var Zr={ROOT:".veryfront",CACHE:".veryfront/cache",KV:".veryfront/kv",LOGS:".veryfront/logs",TMP:".veryfront/tmp"},vo=Zr.CACHE;var wo={HMR_RUNTIME:ge.HMR_RUNTIME,ERROR_OVERLAY:ge.ERROR_OVERLAY};var Qr=w.RSC,en=w.FS;var de="rsc-root",P="x-veryfront-dependency-pins";var Vo=Array.prototype.at,Go=Array.prototype.filter,Fo=Array.prototype.join,Ho=Array.prototype.map,jo=Array.prototype.pop,zo=Array.prototype.push,Yo=Array.prototype.sort;var is=Object.freeze({react:"","react-dom":"","react-dom/client":"","react-dom/server":"","react/jsx-runtime":"","react/jsx-dev-runtime":""});var rn="veryfront-hydration-data";function fe(t){try{let r=[...t.querySelectorAll(`[id="${rn}"]`)];if(r.length!==1)return null;let e=t.body;if(!e)return null;let o=r[0];return e.firstElementChild!==o&&o.parentElement!==e||o.tagName?.toLowerCase()!=="script"||o.getAttribute("type")?.trim().toLowerCase()!=="application/json"?null:o}catch{return null}}function pe(t,r){if(!r?.startsWith("on:"))return!1;try{let e=fe(t);if(!e)return!1;let o=JSON.parse(e.textContent||"{}");return o.dependencyPinningCacheKey=r,e.textContent=JSON.stringify(o),!0}catch(e){return R.debug("hydration dependency snapshot seed failed",e),!1}}function me(t,r){let e=r==="root"?de:`rsc-slot-${r}`,o=t.getElementById(e);if(o)return o;let s=t.createElement("div");return s.id=e,t.body.appendChild(s),s}function nn(t,r){if(r.type!=="slot")return;let e=me(t,r.id);e.innerHTML=ue(String(r.html??""))}function Ee(t,r){let e=r.split(`\n`),o=e.pop()??"";for(let s of e){let i=s.trim();if(!i)continue;let a;try{a=JSON.parse(i)}catch(d){R.debug("[client-dom] malformed NDJSON line",{line:i,error:d instanceof Error?d.message:String(d)});continue}if(!a||typeof a!="object")continue;let c=a;if(c.type==="slot"){nn(t,c);try{an(t,c.id||"root")}catch(d){R.debug("[client-dom] hydration optional failed",d)}}}return o}function on(t){return new Promise((r,e)=>{let o=()=>e(new DOMException("aborted","AbortError"));if(t.aborted){o();return}t.addEventListener("abort",o,{once:!0})})}async function As(t,r=document,e){let o="body"in t?t:null,s=o?.body??t;if(!s)return;o&&pe(r,o.headers.get(P));let i=s.getReader(),a=new TextDecoder,c="",d=!1;try{for(;;){if(e?.aborted)throw new DOMException("aborted","AbortError");let u=i.read(),{done:g,value:f}=e?await Promise.race([u,on(e)]):await u;if(g){d=!0;break}c+=a.decode(f,{stream:!0}),c=Ee(r,c)}c&&Ee(r,`${c}\n`)}catch(u){throw u instanceof Error&&u.name==="AbortError"||R.debug("[client-dom] consumeNdjsonStream error",u),u}finally{try{await i.cancel()}catch(u){d||R.debug("[client-dom] reader.cancel failed",u)}try{i.releaseLock()}catch(u){R.debug("[client-dom] reader.releaseLock failed",u)}if(typeof s.cancel=="function")try{await s.cancel()}catch(u){R.debug("[client-dom] stream.cancel failed",u)}if(typeof o?.body?.cancel=="function")try{await o.body.cancel()}catch(u){R.debug("[client-dom] response.body.cancel failed",u)}}}function sn(t,r){let e=me(t,r),o=[],s=i=>{let a=i;a.dataset?.clientRef&&o.push(a);for(let c of i.children)s(c)};return s(e),o}function an(t,r){let e=sn(t,r);for(let o of e){let s=o.dataset?.clientRef;s&&(o.dataset.hydrated="true",R.debug("[client-dom] marked for hydration",s))}}export{As as consumeNdjsonStream,me as getContainer};\n'; diff --git a/src/transforms/esm/http-cache-helpers.test.ts b/src/transforms/esm/http-cache-helpers.test.ts index 6216bdb683..df1384f750 100644 --- a/src/transforms/esm/http-cache-helpers.test.ts +++ b/src/transforms/esm/http-cache-helpers.test.ts @@ -17,7 +17,6 @@ import { prepareHttpCacheRequestOptions, resolveBareSpecifier, } from "./http-cache-helpers.ts"; - describe("transforms/esm/http-cache-helpers", () => { describe("cache identity", () => { it("uses a full SHA-256 fingerprint for import maps that collide under 32-bit hashing", async () => { @@ -48,6 +47,39 @@ describe("transforms/esm/http-cache-helpers", () => { ); }); + it("does not consult inherited toJSON hooks while fingerprinting", async () => { + const original = Object.getOwnPropertyDescriptor(Array.prototype, "toJSON"); + let first: string; + let second: string; + try { + Object.defineProperty(Array.prototype, "toJSON", { + configurable: true, + value: () => [], + }); + first = await fingerprintImportMap({ imports: { package: "version-a" } }); + second = await fingerprintImportMap({ imports: { package: "version-b" } }); + } finally { + if (original) Object.defineProperty(Array.prototype, "toJSON", original); + else delete (Array.prototype as unknown as { toJSON?: unknown }).toJSON; + } + + assertNotEquals(first, second); + }); + + it("preserves the established v2 canonical fingerprint bytes", async () => { + assertEquals( + await fingerprintImportMap({ + imports: { package: "https://example.com/package.ts" }, + scopes: { + "https://example.com/": { + scoped: "https://example.com/scoped.ts", + }, + }, + }), + "c0cef34844a37f56972214c773cc169cec17fa1fdd05f80add96f1821ff4650a", + ); + }); + it("frames URL and React version components without delimiter collisions", async () => { const importMap = { imports: {}, scopes: {} }; @@ -63,6 +95,245 @@ describe("transforms/esm/http-cache-helpers", () => { ); }); + it("does not consult mutable JSON or array hooks for final identities", async () => { + const importMap = { imports: {}, scopes: {} }; + const baseline = await buildHttpCacheIdentity( + "https://modules.example.com/root.js", + { importMap, reactVersion: "19.0.0" }, + ); + assertEquals( + baseline, + 'veryfront:http-module:v2:["https://modules.example.com/root.js","19.0.0","318ae612f9deb78c22b7ccf3a2d45fe489d63ca499d03712e9df30c41f9c39e5"]', + ); + const originalStringify = JSON.stringify; + const arrayToJson = Object.getOwnPropertyDescriptor(Array.prototype, "toJSON"); + let hookCalls = 0; + let poisoned: string; + + try { + Reflect.set(JSON, "stringify", () => "poisoned"); + Object.defineProperty(Array.prototype, "toJSON", { + configurable: true, + value() { + hookCalls++; + return []; + }, + writable: true, + }); + poisoned = await buildHttpCacheIdentity( + "https://modules.example.com/root.js", + { importMap, reactVersion: "19.0.0" }, + ); + } finally { + Reflect.set(JSON, "stringify", originalStringify); + if (arrayToJson) Object.defineProperty(Array.prototype, "toJSON", arrayToJson); + else Reflect.deleteProperty(Array.prototype, "toJSON"); + } + + assertEquals(poisoned, baseline); + assertEquals(hookCalls, 0); + }); + + it("uses captured request-context and URL primordials for identities", async () => { + const importMap = { imports: {}, scopes: {} }; + const baseline = await buildHttpCacheIdentity( + "https://esm.sh/lodash@4?z=1&a=2", + prepareHttpCacheRequestOptions({ cacheDir: ".cache", importMap }), + ); + const otherBaseline = await buildHttpCacheIdentity( + "https://esm.sh/preact@10?z=2&b=3", + prepareHttpCacheRequestOptions({ cacheDir: ".cache", importMap }), + ); + const objectDefineProperty = Object.defineProperty; + const urlDescriptor = Object.getOwnPropertyDescriptor(globalThis, "URL")!; + const encodeURIComponentDescriptor = Object.getOwnPropertyDescriptor( + globalThis, + "encodeURIComponent", + )!; + const urlPrototype = URL.prototype; + const urlPrototypeDescriptors = Object.getOwnPropertyDescriptors(urlPrototype); + const searchParamsPrototypeDescriptors = Object.getOwnPropertyDescriptors( + URLSearchParams.prototype, + ); + const stringPrototypeDescriptors = Object.getOwnPropertyDescriptors(String.prototype); + const arrayPrototype: object = Array.prototype; + const arrayPrototypeDescriptors = Object.getOwnPropertyDescriptors(arrayPrototype); + const regExpPrototypeDescriptors = Object.getOwnPropertyDescriptors(RegExp.prototype); + let definePropertyCalls = 0; + let urlCalls = 0; + let urlPrototypeCalls = 0; + let poisoned: string; + let otherPoisoned: string; + + try { + objectDefineProperty(Object, "defineProperty", { + configurable: true, + value() { + definePropertyCalls++; + throw new Error("poisoned defineProperty"); + }, + writable: true, + }); + objectDefineProperty(globalThis, "URL", { + configurable: true, + value: class PoisonedURL { + constructor() { + urlCalls++; + throw new Error("poisoned URL"); + } + }, + writable: true, + }); + for (const name of ["hostname", "pathname", "searchParams"]) { + objectDefineProperty(urlPrototype, name, { + configurable: true, + get() { + urlPrototypeCalls++; + throw new Error(`poisoned URL.prototype.${name}`); + }, + set() { + urlPrototypeCalls++; + throw new Error(`poisoned URL.prototype.${name}`); + }, + }); + } + objectDefineProperty(urlPrototype, "toString", { + configurable: true, + value() { + urlPrototypeCalls++; + return "https://evil.invalid/collapsed"; + }, + writable: true, + }); + for (const name of ["get", "has", "set", "sort"]) { + objectDefineProperty(URLSearchParams.prototype, name, { + configurable: true, + value() { + urlPrototypeCalls++; + throw new Error(`poisoned URLSearchParams.prototype.${name}`); + }, + writable: true, + }); + } + for (const name of ["includes", "replace", "split"]) { + objectDefineProperty(String.prototype, name, { + configurable: true, + value() { + urlPrototypeCalls++; + return "https://evil.invalid/collapsed"; + }, + writable: true, + }); + } + for (const name of ["filter", "includes", "join", "push"]) { + objectDefineProperty(arrayPrototype, name, { + configurable: true, + value() { + urlPrototypeCalls++; + throw new Error(`poisoned Array.prototype.${name}`); + }, + writable: true, + }); + } + for (const name of ["exec", "test"]) { + objectDefineProperty(RegExp.prototype, name, { + configurable: true, + value() { + urlPrototypeCalls++; + throw new Error(`poisoned RegExp.prototype.${name}`); + }, + writable: true, + }); + } + objectDefineProperty(globalThis, "encodeURIComponent", { + configurable: true, + value() { + urlPrototypeCalls++; + return "collapsed"; + }, + writable: true, + }); + + poisoned = await buildHttpCacheIdentity( + "https://esm.sh/lodash@4?z=1&a=2", + prepareHttpCacheRequestOptions({ cacheDir: ".cache", importMap }), + ); + otherPoisoned = await buildHttpCacheIdentity( + "https://esm.sh/preact@10?z=2&b=3", + prepareHttpCacheRequestOptions({ cacheDir: ".cache", importMap }), + ); + } finally { + objectDefineProperty(Object, "defineProperty", { + configurable: true, + value: objectDefineProperty, + writable: true, + }); + objectDefineProperty(globalThis, "URL", urlDescriptor); + objectDefineProperty(globalThis, "encodeURIComponent", encodeURIComponentDescriptor); + Object.defineProperties(urlPrototype, urlPrototypeDescriptors); + Object.defineProperties(URLSearchParams.prototype, searchParamsPrototypeDescriptors); + Object.defineProperties(String.prototype, stringPrototypeDescriptors); + Object.defineProperties(arrayPrototype, arrayPrototypeDescriptors); + Object.defineProperties(RegExp.prototype, regExpPrototypeDescriptors); + } + + assertEquals(poisoned, baseline); + assertEquals(otherPoisoned, otherBaseline); + assertNotEquals(poisoned, otherPoisoned); + assertEquals(definePropertyCalls, 0); + assertEquals(urlCalls, 0); + assertEquals(urlPrototypeCalls, 0); + }); + + it("does not consult inherited toJSON hooks while fingerprinting import maps", async () => { + const importMap = { + imports: { pkg: "https://modules.example.com/pkg-v1.js" }, + scopes: { + "https://app.example.com/": { + scoped: "https://modules.example.com/scoped-v1.js", + }, + }, + }; + const baseline = await fingerprintImportMap(importMap); + const arrayToJson = Object.getOwnPropertyDescriptor(Array.prototype, "toJSON"); + const objectToJson = Object.getOwnPropertyDescriptor(Object.prototype, "toJSON"); + let hookCalls = 0; + + try { + Object.defineProperty(Array.prototype, "toJSON", { + configurable: true, + value() { + hookCalls++; + return []; + }, + writable: true, + }); + Object.defineProperty(Object.prototype, "toJSON", { + configurable: true, + value() { + hookCalls++; + return {}; + }, + writable: true, + }); + + assertEquals(await fingerprintImportMap(importMap), baseline); + } finally { + if (arrayToJson) { + Object.defineProperty(Array.prototype, "toJSON", arrayToJson); + } else { + Reflect.deleteProperty(Array.prototype, "toJSON"); + } + if (objectToJson) { + Object.defineProperty(Object.prototype, "toJSON", objectToJson); + } else { + Reflect.deleteProperty(Object.prototype, "toJSON"); + } + } + + assertEquals(hookCalls, 0); + }); + it("canonicalizes and fingerprints one import map once per prepared request graph", async () => { let importEnumerations = 0; const imports = new Proxy({ pkg: "https://modules.example.com/pkg.js" }, { @@ -146,6 +417,21 @@ describe("transforms/esm/http-cache-helpers", () => { }), ); }); + + it("does not externalize prefixed base React package URLs", () => { + assertEquals( + normalizeHttpUrl("https://esm.sh/stable/react@18.3.1"), + "https://esm.sh/stable/react@18.3.1?target=es2022", + ); + assertEquals( + normalizeHttpUrl("https://esm.sh/v135/react@18.3.1"), + "https://esm.sh/v135/react@18.3.1?target=es2022", + ); + assertEquals( + normalizeHttpUrl("https://esm.sh/v135/react-dom@18.3.1/server.js"), + "https://esm.sh/v135/react-dom@18.3.1/server.js?external=react&target=es2022", + ); + }); }); describe("isHttpUrl", () => { @@ -342,11 +628,63 @@ describe("transforms/esm/http-cache-helpers", () => { assertEquals(hasIncompatibleFilePaths(code, "/cache"), false); }); + it("requires bundle paths to stay inside the local cache directory boundary", () => { + assertEquals( + hasIncompatibleFilePaths( + 'import "file:///cache/veryfront-http-bundle/http-123.mjs";', + "/cache", + ), + false, + ); + assertEquals( + hasIncompatibleFilePaths( + 'import "file:///cache-other/veryfront-http-bundle/http-123.mjs";', + "/cache", + ), + true, + ); + }); + it("returns true when bundle paths are from different environment", () => { const code = 'import "file:///other/veryfront-http-bundle/http-123.mjs";'; assertEquals(hasIncompatibleFilePaths(code, "/cache"), true); }); + it("uses captured intrinsics after prototype poisoning", () => { + const code = 'import x from "file:///remote/cache/veryfront-http-bundle/http-deadbeef.mjs";'; + const stringPrototypeDescriptors = Object.getOwnPropertyDescriptors(String.prototype); + const regExpPrototypeDescriptors = Object.getOwnPropertyDescriptors(RegExp.prototype); + + try { + Object.defineProperty(RegExp.prototype, "exec", { + configurable: true, + value() { + return null; + }, + writable: true, + }); + Object.defineProperty(String.prototype, "includes", { + configurable: true, + value() { + return false; + }, + writable: true, + }); + Object.defineProperty(String.prototype, "startsWith", { + configurable: true, + value() { + return true; + }, + writable: true, + }); + + assertEquals(hasIncompatibleFilePaths(code, "/local/cache"), true); + } finally { + Object.defineProperties(String.prototype, stringPrototypeDescriptors); + Object.defineProperties(RegExp.prototype, regExpPrototypeDescriptors); + } + }); + it("ignores non-bundle file:// paths", () => { const code = 'import "file:///other/some-file.js";'; assertEquals(hasIncompatibleFilePaths(code, "/cache"), false); @@ -376,5 +714,37 @@ describe("transforms/esm/http-cache-helpers", () => { const result = resolveBareSpecifier("react-dom/client", emptyImportMap); assertEquals(result.includes("react-dom"), true); }); + + it("uses captured string intrinsics after prototype poisoning", () => { + const stringPrototypeDescriptors = Object.getOwnPropertyDescriptors(String.prototype); + + try { + Object.defineProperty(String.prototype, "startsWith", { + configurable: true, + value() { + return false; + }, + writable: true, + }); + Object.defineProperty(String.prototype, "slice", { + configurable: true, + value() { + return "poisoned"; + }, + writable: true, + }); + + assertEquals(isHttpUrl("https://esm.sh/react@19"), true); + assertEquals(isExternalScheme("file:///tmp/module.js"), true); + assertEquals(isRelative("./local.js"), true); + assertEquals(isInternalBare("veryfront/runtime"), true); + assertEquals( + resolveBareSpecifier("react-dom/client", emptyImportMap, "19.1.0"), + "https://esm.sh/react-dom@19.1.0/client?external=react&target=es2022&deps=csstype@3.2.3", + ); + } finally { + Object.defineProperties(String.prototype, stringPrototypeDescriptors); + } + }); }); }); diff --git a/src/transforms/esm/http-cache-helpers.ts b/src/transforms/esm/http-cache-helpers.ts index a4ee43aecd..e315ecc47c 100644 --- a/src/transforms/esm/http-cache-helpers.ts +++ b/src/transforms/esm/http-cache-helpers.ts @@ -4,8 +4,15 @@ * @module transforms/esm/http-cache-helpers */ -import { isAbsolute, join } from "#veryfront/compat/path/index.ts"; +import { isAbsolute, join, normalize } from "#veryfront/compat/path/index.ts"; import { cwd } from "#veryfront/platform/compat/process.ts"; +import { + primordialArrayFilter as arrayFilter, + primordialArrayJoin as arrayJoin, + primordialArrayMap as arrayMap, + primordialArrayPush as arrayPush, + primordialArraySort as arraySort, +} from "#veryfront/platform/compat/primordials/array.ts"; import { rendererLogger } from "#veryfront/utils"; import { resolveImport } from "#veryfront/modules/import-map/resolver.ts"; import type { ImportMapConfig } from "#veryfront/modules/import-map/types.ts"; @@ -15,6 +22,128 @@ import { DEFAULT_REACT_VERSION, getReactImportMap } from "./react-cdn.ts"; import { computeHash } from "#veryfront/utils/hash-utils.ts"; const logger = rendererLogger.component("http-cache"); +const ArrayIncludes = Array.prototype.includes; +const EncodeURIComponent = encodeURIComponent; +const JSONStringify = JSON.stringify; +const IntrinsicURL = URL; +const IntrinsicURLSearchParams = URLSearchParams; +const ObjectDefineProperty = Object.defineProperty; +const ObjectEntries = Object.entries; +const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const RegExpExec = RegExp.prototype.exec; +const ReflectApply = Reflect.apply; +const StringIncludes = String.prototype.includes; +const StringReplace = String.prototype.replace; +const StringSlice = String.prototype.slice; +const StringSplit = String.prototype.split; +const StringStartsWith = String.prototype.startsWith; +const URLHostnameGet = ObjectGetOwnPropertyDescriptor( + IntrinsicURL.prototype, + "hostname", +)!.get!; +const URLPathnameGet = ObjectGetOwnPropertyDescriptor( + IntrinsicURL.prototype, + "pathname", +)!.get!; +const URLPathnameSet = ObjectGetOwnPropertyDescriptor( + IntrinsicURL.prototype, + "pathname", +)!.set!; +const URLSearchParamsGet = ObjectGetOwnPropertyDescriptor( + IntrinsicURL.prototype, + "searchParams", +)!.get!; +const URLToString = IntrinsicURL.prototype.toString; +const URLSearchParamsGetValue = IntrinsicURLSearchParams.prototype.get; +const URLSearchParamsHas = IntrinsicURLSearchParams.prototype.has; +const URLSearchParamsSetValue = IntrinsicURLSearchParams.prototype.set; +const URLSearchParamsSort = IntrinsicURLSearchParams.prototype.sort; + +function getURLHostname(url: URL): string { + return ReflectApply(URLHostnameGet, url, []); +} + +function getURLPathname(url: URL): string { + return ReflectApply(URLPathnameGet, url, []); +} + +function setURLPathname(url: URL, pathname: string): void { + ReflectApply(URLPathnameSet, url, [pathname]); +} + +function getURLSearchParams(url: URL): URLSearchParams { + return ReflectApply(URLSearchParamsGet, url, []); +} + +function stringifyURL(url: URL): string { + return ReflectApply(URLToString, url, []); +} + +function getURLSearchParam(searchParams: URLSearchParams, name: string): string | null { + return ReflectApply(URLSearchParamsGetValue, searchParams, [name]); +} + +function hasURLSearchParam(searchParams: URLSearchParams, name: string): boolean { + return ReflectApply(URLSearchParamsHas, searchParams, [name]); +} + +function setURLSearchParam(searchParams: URLSearchParams, name: string, value: string): void { + ReflectApply(URLSearchParamsSetValue, searchParams, [name, value]); +} + +function sortURLSearchParams(searchParams: URLSearchParams): void { + ReflectApply(URLSearchParamsSort, searchParams, []); +} + +function arrayIncludesValue(values: readonly T[], value: T): boolean { + return ReflectApply(ArrayIncludes, values, [value]); +} + +function execRegExp(pattern: RegExp, value: string): RegExpExecArray | null { + return ReflectApply(RegExpExec, pattern, [value]); +} + +function testRegExp(pattern: RegExp, value: string): boolean { + return execRegExp(pattern, value) !== null; +} + +function stringIncludes(value: string, search: string): boolean { + return ReflectApply(StringIncludes, value, [search]); +} + +function stringReplace(value: string, search: string, replacement: string): string { + return ReflectApply(StringReplace, value, [search, replacement]); +} + +function stringSlice(value: string, start: number, end?: number): string { + return ReflectApply(StringSlice, value, end === undefined ? [start] : [start, end]); +} + +function stringSplit(value: string, separator: string): string[] { + return ReflectApply(StringSplit, value, [separator]); +} + +function stringStartsWith(value: string, search: string): boolean { + return ReflectApply(StringStartsWith, value, [search]); +} + +function decodeEncodedCommas(value: string): string { + let decoded = ""; + let index = 0; + while (index < value.length) { + if ( + value[index] === "%" && value[index + 1] === "2" && + (value[index + 2] === "C" || value[index + 2] === "c") + ) { + decoded += ","; + index += 3; + continue; + } + decoded += value[index]; + index++; + } + return decoded; +} /** * Cache interface for dependency injection (matches LRU essential methods). @@ -81,24 +210,53 @@ const HTTP_CACHE_FILE_HASH_NAMESPACE = "veryfront:http-module-file:v2"; /** Build an order-independent fingerprint covering imports and scoped imports. */ export function fingerprintImportMap(importMap: ImportMapConfig): Promise { - const imports = Object.entries(importMap.imports ?? {}).sort(compareImportMapKeys); - const scopes = Object.entries(importMap.scopes ?? {}) - .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0) - .map(([scope, scopedImports]) => [ - scope, - Object.entries(scopedImports).sort(compareImportMapKeys), - ]); - - return computeHash( - `${HTTP_IMPORT_MAP_FINGERPRINT_NAMESPACE}\0${JSON.stringify({ imports, scopes })}`, + const imports = arraySort(ObjectEntries(importMap.imports ?? {}), compareImportMapKeys); + const sortedScopes = arraySort( + ObjectEntries(importMap.scopes ?? {}), + ([left], [right]) => left < right ? -1 : left > right ? 1 : 0, + ); + const scopes = arrayMap( + sortedScopes, + ([scope, scopedImports]) => + [ + scope, + arraySort(ObjectEntries(scopedImports), compareImportMapKeys), + ] as const, ); + + // Serialize only string primitives. JSON.stringify on arrays/objects still + // consults inherited toJSON hooks even when the intrinsic function itself + // was captured, which would let project code collapse distinct maps onto a + // shared cache identity. + // Preserve the established JSON byte format exactly so this hardening does + // not invalidate every persisted HTTP module cache entry on deployment. + let canonical = `${HTTP_IMPORT_MAP_FINGERPRINT_NAMESPACE}\0{"imports":[`; + for (let index = 0; index < imports.length; index++) { + const [key, value] = imports[index]!; + if (index > 0) canonical += ","; + canonical += `[${JSONStringify(key)},${JSONStringify(value)}]`; + } + canonical += `],"scopes":[`; + for (let scopeIndex = 0; scopeIndex < scopes.length; scopeIndex++) { + const [scope, mappings] = scopes[scopeIndex] as [string, Array<[string, string]>]; + if (scopeIndex > 0) canonical += ","; + canonical += `[${JSONStringify(scope)},[`; + for (let mappingIndex = 0; mappingIndex < mappings.length; mappingIndex++) { + const [key, value] = mappings[mappingIndex]!; + if (mappingIndex > 0) canonical += ","; + canonical += `[${JSONStringify(key)},${JSONStringify(value)}]`; + } + canonical += "]]"; + } + canonical += "]}"; + return computeHash(canonical); } function attachHttpCacheRequestIdentityContext( options: T, context: HttpCacheRequestIdentityContext, ): T { - Object.defineProperty(options, HTTP_CACHE_REQUEST_IDENTITY_CONTEXT, { + ObjectDefineProperty(options, HTTP_CACHE_REQUEST_IDENTITY_CONTEXT, { configurable: false, enumerable: false, value: context, @@ -156,12 +314,10 @@ export async function buildHttpCacheIdentity( const effective = getEffectiveHttpCacheRequest(url, options); const normalizedUrl = normalizeHttpUrl(effective.url); const importMapFingerprint = await getRequestImportMapFingerprint(url, effective.options); - const components = [ - normalizedUrl, - effective.options.reactVersion ?? null, - importMapFingerprint, - ]; - return `${HTTP_CACHE_IDENTITY_NAMESPACE}:${JSON.stringify(components)}`; + const reactVersion = effective.options.reactVersion; + return `${HTTP_CACHE_IDENTITY_NAMESPACE}:[${JSONStringify(normalizedUrl)},${ + reactVersion === undefined ? "null" : JSONStringify(reactVersion) + },${JSONStringify(importMapFingerprint)}]`; } /** Build recoverable metadata while reusing the request graph's import-map fingerprint. */ @@ -188,7 +344,7 @@ export function ensureAbsoluteDir(path: string): string { } export function isHttpUrl(specifier: string): boolean { - return specifier.startsWith("https://") || specifier.startsWith("http://"); + return stringStartsWith(specifier, "https://") || stringStartsWith(specifier, "http://"); } interface CanonicalReactEsmPackage { @@ -201,13 +357,17 @@ interface CanonicalReactEsmPackage { function parseCanonicalReactEsmPackage(rawUrl: string): CanonicalReactEsmPackage | null { try { - const url = new URL(rawUrl); - if (url.hostname !== "esm.sh") return null; + const url = new IntrinsicURL(rawUrl); + if (getURLHostname(url) !== "esm.sh") return null; - const pathSegments = url.pathname.split("/").filter(Boolean); + const pathSegments = arrayFilter( + stringSplit(getURLPathname(url), "/"), + (segment) => segment.length > 0, + ); const prefix = pathSegments[0] ?? ""; - const packageIndex = prefix === "stable" || /^v\d+$/.test(prefix) ? 1 : 0; - const match = /^(react|react-dom)@(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)$/.exec( + const packageIndex = prefix === "stable" || testRegExp(/^v\d+$/, prefix) ? 1 : 0; + const match = execRegExp( + /^(react|react-dom)@(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)$/, pathSegments[packageIndex] ?? "", ); if (!match?.[1] || !match[2]) return null; @@ -244,7 +404,7 @@ export function getEffectiveHttpCacheRequest const version = options.reactVersion ?? parsed.version; if (version !== parsed.version) { parsed.pathSegments[parsed.packageIndex] = `${parsed.packageName}@${version}`; - parsed.url.pathname = `/${parsed.pathSegments.join("/")}`; + setURLPathname(parsed.url, `/${arrayJoin(parsed.pathSegments, "/")}`); } const effectiveOptions = { @@ -255,7 +415,7 @@ export function getEffectiveHttpCacheRequest const context = getHttpCacheRequestIdentityContext(options); if (context) attachHttpCacheRequestIdentityContext(effectiveOptions, context); - return { url: parsed.url.toString(), options: effectiveOptions }; + return { url: stringifyURL(parsed.url), options: effectiveOptions }; } /** @@ -270,15 +430,17 @@ export function isCanonicalReactEsmUrl(rawUrl: string): boolean { } export function isExternalScheme(specifier: string): boolean { - return specifier.startsWith("node:") || - specifier.startsWith("data:") || - specifier.startsWith("file:") || - specifier.startsWith("bun:") || - specifier.startsWith("jsr:"); + return stringStartsWith(specifier, "node:") || + stringStartsWith(specifier, "data:") || + stringStartsWith(specifier, "file:") || + stringStartsWith(specifier, "bun:") || + stringStartsWith(specifier, "jsr:"); } export function isRelative(specifier: string): boolean { - return specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/"); + return stringStartsWith(specifier, "./") || + stringStartsWith(specifier, "../") || + stringStartsWith(specifier, "/"); } /** @@ -290,57 +452,62 @@ export function isParentHttpModule(baseUrl: string | undefined): boolean { } export function isInternalBare(specifier: string): boolean { - return specifier.startsWith("veryfront/") || - specifier.startsWith("#") || - specifier.startsWith("@std/") || - specifier.startsWith("_vf_modules/") || - specifier.startsWith("/_vf_modules/") || - specifier.startsWith("_veryfront/") || - specifier.startsWith("/_veryfront/"); + return stringStartsWith(specifier, "veryfront/") || + stringStartsWith(specifier, "#") || + stringStartsWith(specifier, "@std/") || + stringStartsWith(specifier, "_vf_modules/") || + stringStartsWith(specifier, "/_vf_modules/") || + stringStartsWith(specifier, "_veryfront/") || + stringStartsWith(specifier, "/_veryfront/"); } export function normalizeEsmShUrl(url: URL): void { - if (url.hostname !== "esm.sh") return; + if (getURLHostname(url) !== "esm.sh") return; - if (url.pathname.includes("/denonext/")) { - url.pathname = url.pathname.replace("/denonext/", "/"); + const originalPathname = getURLPathname(url); + if (stringIncludes(originalPathname, "/denonext/")) { + setURLPathname(url, stringReplace(originalPathname, "/denonext/", "/")); } - if (!url.searchParams.has("target")) { - url.searchParams.set("target", "es2022"); + const searchParams = getURLSearchParams(url); + if (!hasURLSearchParam(searchParams, "target")) { + setURLSearchParam(searchParams, "target", "es2022"); } - const pathname = url.pathname.replace(/^\/+/, ""); - const isBaseReact = /^react@[\d.]+(?:\?|$)/.test(pathname); + const canonicalReact = parseCanonicalReactEsmPackage(stringifyURL(url)); + const isBaseReact = canonicalReact?.packageName === "react" && + canonicalReact.pathSegments.length === canonicalReact.packageIndex + 1; if (isBaseReact) return; - const existing = url.searchParams.get("external"); - const externals = existing ? existing.split(",") : []; - if (!externals.includes("react")) { - externals.push("react"); - url.searchParams.set("external", externals.join(",")); + const existing = getURLSearchParam(searchParams, "external"); + const externals = existing ? stringSplit(existing, ",") : []; + if (!arrayIncludesValue(externals, "react")) { + arrayPush(externals, "react"); + setURLSearchParam(searchParams, "external", arrayJoin(externals, ",")); } } export function normalizeHttpUrl(raw: string): string { try { - const url = new URL(raw); + const url = new IntrinsicURL(raw); normalizeEsmShUrl(url); - url.searchParams.sort(); - const normalized = url.toString(); + const searchParams = getURLSearchParams(url); + sortURLSearchParams(searchParams); + const normalized = stringifyURL(url); // esm.sh misbehaves when list-valued params such as // `external=react,react-dom` are percent-encoded as `%2C`. // Preserve literal commas only for the affected param so unrelated // query values remain canonically encoded. - if (url.hostname === "esm.sh") { - const external = url.searchParams.get("external"); + if (getURLHostname(url) === "esm.sh") { + const external = getURLSearchParam(searchParams, "external"); if (!external) return normalized; - const encodedExternal = encodeURIComponent(external); - return normalized.replace( + const encodedExternal = EncodeURIComponent(external); + return stringReplace( + normalized, `external=${encodedExternal}`, - `external=${encodedExternal.replace(/%2C/gi, ",")}`, + `external=${decodeEncodedCommas(encodedExternal)}`, ); } @@ -360,13 +527,13 @@ export function resolveBareSpecifier( const reactMapped = reactMap[specifier]; if (reactMapped) return reactMapped; - if (specifier.startsWith("react/")) { - const subpath = specifier.slice("react/".length); + if (stringStartsWith(specifier, "react/")) { + const subpath = stringSlice(specifier, "react/".length); return `https://esm.sh/react@${reactVersion}/${subpath}?external=react&target=es2022`; } - if (specifier.startsWith("react-dom/")) { - const subpath = specifier.slice("react-dom/".length); + if (stringStartsWith(specifier, "react-dom/")) { + const subpath = stringSlice(specifier, "react-dom/".length); return `https://esm.sh/react-dom@${reactVersion}/${subpath}?external=react&target=es2022`; } @@ -391,16 +558,18 @@ export function resolveBareSpecifier( */ export function hasIncompatibleFilePaths(code: string, localCacheDir: string): boolean { const filePathPattern = /file:\/\/([^"'\s]+)/gi; + const expectedCacheRoot = normalize(localCacheDir); + const expectedCacheChildPrefix = `${expectedCacheRoot}/`; let match: RegExpExecArray | null; - while ((match = filePathPattern.exec(code)) !== null) { + while ((match = execRegExp(filePathPattern, code)) !== null) { const path = match[1]!; - if (!path.includes("veryfront-http-bundle")) continue; + if (!stringIncludes(path, "veryfront-http-bundle")) continue; - if (!path.startsWith(localCacheDir)) { + if (path !== expectedCacheRoot && !stringStartsWith(path, expectedCacheChildPrefix)) { logger.debug("Bundle has incompatible file path from different environment", { path, - expectedDir: localCacheDir, + expectedDir: expectedCacheRoot, }); return true; } diff --git a/src/transforms/esm/specifier-resolver.test.ts b/src/transforms/esm/specifier-resolver.test.ts index 0ae9a5b057..6680e29aed 100644 --- a/src/transforms/esm/specifier-resolver.test.ts +++ b/src/transforms/esm/specifier-resolver.test.ts @@ -93,6 +93,28 @@ describe("transforms/esm/specifier-resolver", () => { assertEquals(result.replacements.get("npm:react@18"), "react@18"); }); + it("resolves npm: specifiers after String prefix poisoning", async () => { + const stringPrototypeDescriptors = Object.getOwnPropertyDescriptors(String.prototype); + + try { + Object.defineProperty(String.prototype, "startsWith", { + configurable: true, + value() { + throw new Error("poisoned String.prototype.startsWith"); + }, + writable: true, + }); + + const code = `import React from "npm:react@18";`; + const result = await buildReplacements(code, undefined, defaultOptions, async () => { + return "/tmp/cache/http-12345.mjs"; + }); + assertEquals(result.replacements.get("npm:react@18"), "file:///tmp/cache/http-12345.mjs"); + } finally { + Object.defineProperties(String.prototype, stringPrototypeDescriptors); + } + }); + it("rewrites http URL when cache returns a path", async () => { const code = `import lodash from "https://esm.sh/lodash@4";`; const mockCache: CacheHttpModuleFn = async () => "/tmp/cache/http-99999.mjs"; diff --git a/src/transforms/esm/specifier-resolver.ts b/src/transforms/esm/specifier-resolver.ts index c69d099787..279859a035 100644 --- a/src/transforms/esm/specifier-resolver.ts +++ b/src/transforms/esm/specifier-resolver.ts @@ -26,13 +26,25 @@ import { resolveBareSpecifier, } from "./http-cache-helpers.ts"; +const ReflectApply = Reflect.apply; +const StringSlice = String.prototype.slice; +const StringStartsWith = String.prototype.startsWith; + +function stringSlice(value: string, start: number, end?: number): string { + return ReflectApply(StringSlice, value, end === undefined ? [start] : [start, end]) as string; +} + +function stringStartsWith(value: string, search: string): boolean { + return ReflectApply(StringStartsWith, value, [search]) as boolean; +} + /** Function signature for caching an HTTP module and returning its local path. */ export type CacheHttpModuleFn = (url: string, options: CacheOptions) => Promise; function isLocalMappedSpecifier(specifier: string): boolean { - return specifier.startsWith("/_vf_modules/") || - specifier.startsWith("_vf_modules/") || - specifier.startsWith("file://"); + return stringStartsWith(specifier, "/_vf_modules/") || + stringStartsWith(specifier, "_vf_modules/") || + stringStartsWith(specifier, "file://"); } /** @@ -56,7 +68,9 @@ async function resolveSpecifier( // configured code path, so leaving the specifier external lets the runtime // resolve the real package (node_modules on Node, npm: on Deno) if and when // the backend is actually used — and costs nothing when it is not. - const serverOnlyCandidate = specifier.startsWith("npm:") ? specifier.slice(4) : specifier; + const serverOnlyCandidate = stringStartsWith(specifier, "npm:") + ? stringSlice(specifier, 4) + : specifier; const serverOnlyParsed = parseBarePackageSpecifier(serverOnlyCandidate); if (serverOnlyParsed && isServerOnlyPackage(serverOnlyParsed.packageName)) return null; @@ -67,8 +81,8 @@ async function resolveSpecifier( return resolveSpecifier(mapped, baseUrl, options, cacheHttpModule); } - if (specifier.startsWith("npm:")) { - const bareSpecifier = specifier.slice(4); + if (stringStartsWith(specifier, "npm:")) { + const bareSpecifier = stringSlice(specifier, 4); const cached = await cacheHttpModule(`https://esm.sh/${bareSpecifier}`, options); if (!cached) return bareSpecifier; @@ -107,7 +121,7 @@ async function resolveSpecifier( } if (isRelative(specifier)) { - if (specifier.startsWith("/_vf_modules/")) return null; + if (stringStartsWith(specifier, "/_vf_modules/")) return null; if (!baseUrl || !isHttpUrl(baseUrl)) return null; const resolved = new URL(specifier, baseUrl).toString(); diff --git a/src/transforms/esm/types.ts b/src/transforms/esm/types.ts index 3430871b36..3c884f4f3f 100644 --- a/src/transforms/esm/types.ts +++ b/src/transforms/esm/types.ts @@ -1,5 +1,7 @@ import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; import type { DependencyHashCache } from "#veryfront/cache/dependency-graph.ts"; +import type { PreloadImportMapContext } from "#veryfront/modules/import-map/preloader.ts"; +import type { ImportMapConfig } from "#veryfront/modules/import-map/types.ts"; import type { TransformProgressListener } from "#veryfront/transforms/progress.ts"; import type { DependencyPinningSourceInput } from "./package-registry.ts"; import type { DependencyResolutionObservation } from "../import-rewriter/dependency-resolution.ts"; @@ -17,6 +19,12 @@ export interface TransformOptions { studioEmbed?: boolean; /** React version for transforms (from project config, defaults to DEFAULT_REACT_VERSION) */ reactVersion?: string; + /** Immutable import-map snapshot already selected for this render. */ + preloadedImportMap?: ImportMapConfig; + /** Adapter used to load and cache the project import map before SSR cache identity. */ + importMapAdapter?: RuntimeAdapter; + /** Content-source/config identity for the import-map preloader. */ + importMapPreloadContext?: PreloadImportMapContext; /** Internal per-render dependency hash cache. */ dependencyHashCache?: DependencyHashCache; /** Internal stable flag + package dependency-map key for cache isolation. */ diff --git a/src/transforms/import-rewriter/url-builder.test.ts b/src/transforms/import-rewriter/url-builder.test.ts index 179da3f110..e0a649816a 100644 --- a/src/transforms/import-rewriter/url-builder.test.ts +++ b/src/transforms/import-rewriter/url-builder.test.ts @@ -250,11 +250,16 @@ describe("transforms/import-rewriter/url-builder", () => { "react/jsx-runtime", "react/jsx-dev-runtime", "react/", + "react-dom/", ] as const; for (const key of keys) { assertEquals(typeof map[key], "string"); } + + assertEquals(map["react/"]?.endsWith("/"), true); + assertEquals(map["react-dom/"]?.endsWith("/"), true); + assertEquals(map["react-dom/"]?.includes("&external=react"), true); }); }); diff --git a/src/transforms/import-rewriter/url-builder.ts b/src/transforms/import-rewriter/url-builder.ts index 2097e902cc..5b79589397 100644 --- a/src/transforms/import-rewriter/url-builder.ts +++ b/src/transforms/import-rewriter/url-builder.ts @@ -5,6 +5,11 @@ * Ensures consistent URLs across SSR and browser for hydration parity. */ +import { + primordialArrayJoin as arrayJoin, + primordialArrayPush as arrayPush, +} from "#veryfront/platform/compat/primordials/array.ts"; + /** * Default React version - used when not specified. * @@ -27,6 +32,30 @@ type EsmShOptions = { deps?: Record; }; +const ObjectEntries = Object.entries; + +function buildEsmShParams(options?: EsmShOptions): string[] { + const params: string[] = []; + + if (options?.external?.length) { + arrayPush(params, `external=${arrayJoin(options.external, ",")}`); + } + + arrayPush(params, `target=${options?.target ?? "es2022"}`); + + if (options?.deps) { + const deps: string[] = []; + const entries = ObjectEntries(options.deps); + for (let index = 0; index < entries.length; index++) { + const [key, value] = entries[index]!; + arrayPush(deps, `${key}@${value}`); + } + arrayPush(params, `deps=${arrayJoin(deps, ",")}`); + } + + return params; +} + /** * Build esm.sh URL with proper configuration. * @@ -41,28 +70,29 @@ export function buildEsmShUrl( subpath?: string, options?: EsmShOptions, ): string { - const params: string[] = []; - - if (options?.external?.length) { - params.push(`external=${options.external.join(",")}`); - } - - params.push(`target=${options?.target ?? "es2022"}`); - - if (options?.deps) { - const depsStr = Object.entries(options.deps) - .map(([k, v]) => `${k}@${v}`) - .join(","); - params.push(`deps=${depsStr}`); - } + const params = buildEsmShParams(options); const versionStr = version ? `@${version}` : ""; const pathStr = subpath ?? ""; - const queryStr = params.length ? `?${params.join("&")}` : ""; + const queryStr = params.length ? `?${arrayJoin(params, "&")}` : ""; return `https://esm.sh/${pkg}${versionStr}${pathStr}${queryStr}`; } +/** + * Build an esm.sh package-prefix URL. esm.sh's `&option/` form keeps the + * trailing slash required by the import-map prefix-matching algorithm. + */ +function buildEsmShPrefixUrl( + pkg: string, + version: string, + options?: EsmShOptions, +): string { + const params = buildEsmShParams(options); + const optionStr = params.length ? `&${arrayJoin(params, "&")}` : ""; + return `https://esm.sh/${pkg}@${version}${optionStr}/`; +} + /** * Build React esm.sh URL. * Uses deps=csstype for type consistency. @@ -79,6 +109,16 @@ export function buildReactUrl( }); } +function buildReactPrefixUrl( + pkg: "react" | "react-dom", + version: string, +): string { + return buildEsmShPrefixUrl(pkg, version, { + external: ["react"], + deps: { csstype: CSSTYPE_VERSION }, + }); +} + /** * Get complete React import map for a specific version. */ @@ -90,8 +130,10 @@ export function getReactImportMap(version: string): Record { "react-dom/server": buildReactUrl("react-dom", version, "/server", true), "react/jsx-runtime": buildReactUrl("react", version, "/jsx-runtime", true), "react/jsx-dev-runtime": buildReactUrl("react", version, "/jsx-dev-runtime", true), - // Prefix match for any react/* subpath imports - "react/": buildReactUrl("react", version, "/", true), + // Prefix matches cover future package exports without allowing a project + // import map to redirect React or ReactDOM subpaths. + "react/": buildReactPrefixUrl("react", version), + "react-dom/": buildReactPrefixUrl("react-dom", version), }; } diff --git a/src/transforms/pipeline/cache-identity.test.ts b/src/transforms/pipeline/cache-identity.test.ts new file mode 100644 index 0000000000..2a1da5ce73 --- /dev/null +++ b/src/transforms/pipeline/cache-identity.test.ts @@ -0,0 +1,596 @@ +import "#veryfront/schemas/_test-setup.ts"; +import { + assertEquals, + assertNotEquals, + assertRejects, + assertThrows, +} from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; +import { + computePipelineConfigIdentity, + fingerprintPipelineImportMap, + getCustomPluginCacheIdentity, + snapshotImportMap, +} from "./cache-identity.ts"; +import { type TransformPlugin, TransformStage } from "./types.ts"; + +const transform = (ctx: { code: string }): string => ctx.code; + +function identityInput( + overrides: Partial[0]> = {}, +) { + return { + reactVersion: "19.1.0", + jsxImportSource: "react", + studioEmbed: false, + dev: false, + ssr: true, + projectDir: "/project", + importMapFingerprint: "a".repeat(64), + customPlugins: [], + ...overrides, + }; +} + +describe("transform pipeline cache identity", () => { + it("snapshots import maps without invoking getters", () => { + let getterCalls = 0; + const imports = Object.create(null) as Record; + Object.defineProperty(imports, "danger", { + enumerable: true, + get() { + getterCalls++; + return "/project/danger.ts"; + }, + }); + + assertThrows( + () => snapshotImportMap({ imports }), + TypeError, + "accessor properties", + ); + assertEquals(getterCalls, 0); + }); + + it("uses an immutable import-map snapshot", () => { + const raw = { imports: { local: "/project/v1.ts" } }; + const snapshot = snapshotImportMap(raw); + raw.imports.local = "/project/v2.ts"; + + assertEquals(snapshot.imports?.local, "/project/v1.ts"); + assertEquals(Object.isFrozen(snapshot), true); + assertEquals(Object.isFrozen(snapshot.imports), true); + }); + + it("rejects import maps that exceed the entry budget", () => { + const imports = Object.create(null) as Record; + for (let index = 0; index <= 20_000; index++) { + imports[`package-${index}`] = `/package-${index}.ts`; + } + + assertThrows( + () => snapshotImportMap({ imports }), + TypeError, + "too many entries", + ); + }); + + it("rejects import-map strings that exceed the per-field byte budget", () => { + assertThrows( + () => + snapshotImportMap({ + imports: { package: "x".repeat(64 * 1024 + 1) }, + }), + TypeError, + "too large", + ); + }); + + it("fingerprints import maps independent of insertion order", async () => { + const first = snapshotImportMap({ imports: { a: "/a.ts", b: "/b.ts" } }); + const reordered = snapshotImportMap({ imports: { b: "/b.ts", a: "/a.ts" } }); + const changed = snapshotImportMap({ imports: { a: "/a.ts", b: "/v2.ts" } }); + + assertEquals( + await fingerprintPipelineImportMap(first), + await fingerprintPipelineImportMap(reordered), + ); + assertNotEquals( + await fingerprintPipelineImportMap(first), + await fingerprintPipelineImportMap(changed), + ); + }); + + it("preserves nonempty import-map identity after array iterator poisoning", async () => { + const originalArrayIterator = Array.prototype[Symbol.iterator]; + let snapshot: ReturnType | undefined; + try { + Reflect.set( + Array.prototype, + Symbol.iterator, + () => ({ next: () => ({ done: true, value: undefined }) }), + ); + snapshot = snapshotImportMap({ + imports: { package: "https://example.com/package.ts" }, + scopes: { + "https://example.com/": { + scoped: "https://example.com/scoped.ts", + }, + }, + }); + } finally { + Reflect.set(Array.prototype, Symbol.iterator, originalArrayIterator); + } + + assertEquals(snapshot?.imports?.package, "https://example.com/package.ts"); + assertEquals( + snapshot?.scopes?.["https://example.com/"]?.scoped, + "https://example.com/scoped.ts", + ); + assertNotEquals( + await fingerprintPipelineImportMap(snapshot), + await fingerprintPipelineImportMap(snapshotImportMap({})), + ); + }); + + it("disables persistent caching for unidentified custom plugins", () => { + const plugin: TransformPlugin = { + name: "custom", + stage: TransformStage.FINALIZE, + transform, + }; + + assertEquals(getCustomPluginCacheIdentity([plugin]).cacheable, false); + plugin.cacheIdentity = "custom@1"; + assertEquals(getCustomPluginCacheIdentity([plugin]).cacheable, true); + }); + + it("rejects accessor-backed plugin identities without invoking them", () => { + let getterCalls = 0; + const plugin = { + name: "custom", + stage: TransformStage.FINALIZE, + transform, + } as TransformPlugin; + Object.defineProperty(plugin, "cacheIdentity", { + enumerable: true, + get() { + getterCalls++; + return "custom@1"; + }, + }); + + assertThrows( + () => getCustomPluginCacheIdentity([plugin]), + TypeError, + "accessor properties", + ); + assertEquals(getterCalls, 0); + }); + + it("accepts class-style plugins with methods on the prototype", () => { + class ClassPlugin implements TransformPlugin { + name = "class-plugin"; + stage = TransformStage.FINALIZE; + cacheIdentity = "class-plugin@1"; + + transform(ctx: { code: string }): string { + return ctx.code; + } + } + + const result = getCustomPluginCacheIdentity([new ClassPlugin()]); + assertEquals(result.cacheable, true); + if (!result.cacheable) { + throw new Error("Expected a cacheable plugin identity"); + } + assertEquals(result.identity, [[ + 0, + "class-plugin", + TransformStage.FINALIZE, + "class-plugin@1", + ]]); + }); + + it("rejects accessor-backed prototype plugin fields without invoking them", () => { + let getterCalls = 0; + const plugin = Object.create({ + name: "custom", + stage: TransformStage.FINALIZE, + cacheIdentity: "custom@1", + get transform() { + getterCalls++; + return transform; + }, + }) as TransformPlugin; + + assertThrows( + () => getCustomPluginCacheIdentity([plugin]), + TypeError, + "accessor properties", + ); + assertEquals(getterCalls, 0); + }); + + it("rejects control characters in plugin names used for logs and spans", () => { + const plugin: TransformPlugin = { + name: "custom\nforged-stage", + stage: TransformStage.FINALIZE, + cacheIdentity: "custom@1", + transform, + }; + + assertThrows(() => getCustomPluginCacheIdentity([plugin]), TypeError, "invalid name"); + }); + + it("accepts fractional custom plugin stages between enum anchors", () => { + const plugin: TransformPlugin = { + name: "custom", + stage: TransformStage.RESOLVE_ALIASES + 0.5, + cacheIdentity: "custom@1", + transform, + }; + + const result = getCustomPluginCacheIdentity([plugin]); + assertEquals(result.cacheable, true); + if (result.cacheable) { + assertEquals(result.identity, [[ + 0, + "custom", + TransformStage.RESOLVE_ALIASES + 0.5, + "custom@1", + ]]); + } + }); + + it("rejects non-finite and unreasonably large custom plugin stages", () => { + for (const stage of [NaN, Infinity, -Infinity, 1_000_001, -1_000_001]) { + const plugin = { + name: "custom", + stage, + cacheIdentity: "custom@1", + transform, + } as TransformPlugin; + + assertThrows(() => getCustomPluginCacheIdentity([plugin]), TypeError, "invalid stage"); + } + }); + + it("rejects oversized base identity fields before hashing", async () => { + await assertRejects( + () => + computePipelineConfigIdentity( + identityInput({ reactVersion: "x".repeat(64 * 1024 + 1) }), + ), + TypeError, + "React version is too large", + ); + }); + + it("changes when any output-affecting endpoint or plugin identity changes", async () => { + const baseline = await computePipelineConfigIdentity(identityInput()); + const moduleServer = await computePipelineConfigIdentity( + identityInput({ moduleServerUrl: "https://modules.example/v1" }), + ); + const api = await computePipelineConfigIdentity( + identityInput({ apiBaseUrl: "https://api.example/v1" }), + ); + const plugins = await computePipelineConfigIdentity( + identityInput({ customPlugins: [[0, "custom", TransformStage.FINALIZE, "custom@1"]] }), + ); + + assertNotEquals(moduleServer, baseline); + assertNotEquals(api, baseline); + assertNotEquals(plugins, baseline); + }); + + it("keeps distinct fractional stages distinct in precomputed identities", async () => { + const early = await computePipelineConfigIdentity( + identityInput({ + customPlugins: [[0, "custom", TransformStage.COMPILE + 0.5, "custom@1"]], + }), + ); + const late = await computePipelineConfigIdentity( + identityInput({ + customPlugins: [[0, "custom", TransformStage.COMPILE + 0.7, "custom@1"]], + }), + ); + + assertNotEquals(early, late); + }); + + it("changes identity when moduleServerOrigin changes", async () => { + const baseline = await computePipelineConfigIdentity( + identityInput({ moduleServerOrigin: "https://app.example.test" }), + ); + const changed = await computePipelineConfigIdentity( + identityInput({ moduleServerOrigin: "https://preview.example.test" }), + ); + + assertNotEquals(changed, baseline); + }); + + it("changes identity when dependencyPinningCacheKey changes", async () => { + const baseline = await computePipelineConfigIdentity( + identityInput({ dependencyPinningCacheKey: "on:first" }), + ); + const changed = await computePipelineConfigIdentity( + identityInput({ dependencyPinningCacheKey: "on:second" }), + ); + + assertNotEquals(changed, baseline); + }); + + it("keeps ill-formed Unicode distinct from replacement characters", async () => { + const loneSurrogate = await computePipelineConfigIdentity( + identityInput({ projectDir: "\ud800" }), + ); + const replacementCharacter = await computePipelineConfigIdentity( + identityInput({ projectDir: "\ufffd" }), + ); + + assertNotEquals(loneSurrogate, replacementCharacter); + }); + + it("does not consult inherited toJSON hooks while hashing", async () => { + const customPlugins = [[0, "custom", TransformStage.FINALIZE, "custom@1"]] as const; + const baseline = await computePipelineConfigIdentity(identityInput({ customPlugins })); + const importMapSnapshot = snapshotImportMap({ + imports: { react: "https://esm.sh/react@19.1.0" }, + scopes: { "/scope/": { dep: "https://example.com/dep.ts" } }, + }); + const fingerprintBaseline = await fingerprintPipelineImportMap(importMapSnapshot); + const distinctSnapshot = snapshotImportMap({ + imports: { react: "https://esm.sh/react@18.3.1" }, + }); + const distinctFingerprintBaseline = await fingerprintPipelineImportMap(distinctSnapshot); + const arrayToJson = Object.getOwnPropertyDescriptor(Array.prototype, "toJSON"); + const objectToJson = Object.getOwnPropertyDescriptor(Object.prototype, "toJSON"); + const stringToJson = Object.getOwnPropertyDescriptor(String.prototype, "toJSON"); + let hookCalls = 0; + + try { + Object.defineProperty(Array.prototype, "toJSON", { + configurable: true, + value() { + hookCalls++; + return ["poisoned-array"]; + }, + writable: true, + }); + Object.defineProperty(Object.prototype, "toJSON", { + configurable: true, + value() { + hookCalls++; + return { poisoned: true }; + }, + writable: true, + }); + Object.defineProperty(String.prototype, "toJSON", { + configurable: true, + value() { + hookCalls++; + return "poisoned-string"; + }, + writable: true, + }); + + assertEquals( + await computePipelineConfigIdentity(identityInput({ customPlugins })), + baseline, + ); + // Poisoned toJSON hooks must neither move nor collapse import-map + // fingerprints: distinct maps stay distinct under poisoning. + assertEquals( + await fingerprintPipelineImportMap(importMapSnapshot), + fingerprintBaseline, + ); + assertEquals( + await fingerprintPipelineImportMap(distinctSnapshot), + distinctFingerprintBaseline, + ); + assertNotEquals( + await fingerprintPipelineImportMap(importMapSnapshot), + await fingerprintPipelineImportMap(distinctSnapshot), + ); + } finally { + if (arrayToJson) { + Object.defineProperty(Array.prototype, "toJSON", arrayToJson); + } else { + Reflect.deleteProperty(Array.prototype, "toJSON"); + } + if (objectToJson) { + Object.defineProperty(Object.prototype, "toJSON", objectToJson); + } else { + Reflect.deleteProperty(Object.prototype, "toJSON"); + } + if (stringToJson) { + Object.defineProperty(String.prototype, "toJSON", stringToJson); + } else { + Reflect.deleteProperty(String.prototype, "toJSON"); + } + } + + assertEquals(hookCalls, 0); + }); + + it("uses captured primordials for import-map and plugin identities", async () => { + const original = { + arrayIsArray: Array.isArray, + arrayMap: Array.prototype.map, + arrayPush: Array.prototype.push, + arraySort: Array.prototype.sort, + jsonStringify: JSON.stringify, + mathAbs: Math.abs, + numberIsFinite: Number.isFinite, + objectCreate: Object.create, + objectEntries: Object.entries, + objectFreeze: Object.freeze, + objectGetOwnPropertyDescriptor: Object.getOwnPropertyDescriptor, + objectGetPrototypeOf: Object.getPrototypeOf, + reflectOwnKeys: Reflect.ownKeys, + regexpTest: RegExp.prototype.test, + rangeError: RangeError, + stringTrim: String.prototype.trim, + textEncoderEncode: TextEncoder.prototype.encode, + typeError: TypeError, + uint8ArrayByteLength: Object.getOwnPropertyDescriptor( + Uint8Array.prototype, + "byteLength", + ), + uint8ArrayLength: Object.getOwnPropertyDescriptor( + Uint8Array.prototype, + "length", + ), + }; + const rawImportMap = { + imports: { package: "https://example.com/package.ts" }, + scopes: { + "https://example.com/": { + scoped: "https://example.com/scoped.ts", + }, + }, + }; + const plugin: TransformPlugin = { + name: "custom", + stage: TransformStage.FINALIZE, + cacheIdentity: "custom@1", + transform, + }; + + let snapshot: ReturnType | undefined; + let fingerprint: string | undefined; + let pluginIdentity: ReturnType | undefined; + let pipelineIdentity: string | undefined; + let invalidMapError: unknown; + let oversizedPluginListError: unknown; + try { + Reflect.set(Array, "isArray", () => false); + Reflect.set(Array.prototype, "map", () => { + throw new Error("poisoned Array.prototype.map"); + }); + Reflect.set(Array.prototype, "push", () => 0); + Reflect.set(Array.prototype, "sort", () => { + throw new Error("poisoned Array.prototype.sort"); + }); + Reflect.set(JSON, "stringify", () => { + throw new Error("poisoned JSON.stringify"); + }); + Reflect.set(Math, "abs", () => Number.POSITIVE_INFINITY); + Reflect.set(Number, "isFinite", () => false); + Reflect.set(Object, "create", () => { + throw new Error("poisoned Object.create"); + }); + Reflect.set(Object, "entries", () => { + throw new Error("poisoned Object.entries"); + }); + Reflect.set(Object, "freeze", (value: T): T => value); + Reflect.set(Object, "getOwnPropertyDescriptor", () => { + throw new Error("poisoned Object.getOwnPropertyDescriptor"); + }); + Reflect.set(Object, "getPrototypeOf", () => null); + Reflect.set(Reflect, "ownKeys", () => []); + Reflect.set(RegExp.prototype, "test", () => true); + Reflect.set( + globalThis, + "RangeError", + class PoisonedRangeError extends Error {}, + ); + Reflect.set(String.prototype, "trim", () => "poisoned"); + Reflect.set(TextEncoder.prototype, "encode", () => { + throw new Error("poisoned TextEncoder.encode"); + }); + Reflect.set( + globalThis, + "TypeError", + class PoisonedTypeError extends Error {}, + ); + Object.defineProperty(Uint8Array.prototype, "byteLength", { + configurable: true, + get: () => 0, + }); + Object.defineProperty(Uint8Array.prototype, "length", { + configurable: true, + get: () => 0, + }); + + snapshot = snapshotImportMap(rawImportMap); + [fingerprint, pipelineIdentity] = await Promise.all([ + fingerprintPipelineImportMap(snapshot), + computePipelineConfigIdentity(identityInput()), + ]); + pluginIdentity = getCustomPluginCacheIdentity([plugin]); + try { + snapshotImportMap(null); + } catch (error) { + invalidMapError = error; + } + try { + getCustomPluginCacheIdentity( + new Array(1_001) as TransformPlugin[], + ); + } catch (error) { + oversizedPluginListError = error; + } + } finally { + Reflect.set(Array, "isArray", original.arrayIsArray); + Reflect.set(Array.prototype, "map", original.arrayMap); + Reflect.set(Array.prototype, "push", original.arrayPush); + Reflect.set(Array.prototype, "sort", original.arraySort); + Reflect.set(JSON, "stringify", original.jsonStringify); + Reflect.set(Math, "abs", original.mathAbs); + Reflect.set(Number, "isFinite", original.numberIsFinite); + Reflect.set(Object, "create", original.objectCreate); + Reflect.set(Object, "entries", original.objectEntries); + Reflect.set(Object, "freeze", original.objectFreeze); + Reflect.set( + Object, + "getOwnPropertyDescriptor", + original.objectGetOwnPropertyDescriptor, + ); + Reflect.set(Object, "getPrototypeOf", original.objectGetPrototypeOf); + Reflect.set(Reflect, "ownKeys", original.reflectOwnKeys); + Reflect.set(RegExp.prototype, "test", original.regexpTest); + Reflect.set(globalThis, "RangeError", original.rangeError); + Reflect.set(String.prototype, "trim", original.stringTrim); + Reflect.set(TextEncoder.prototype, "encode", original.textEncoderEncode); + Reflect.set(globalThis, "TypeError", original.typeError); + if (original.uint8ArrayByteLength) { + Object.defineProperty( + Uint8Array.prototype, + "byteLength", + original.uint8ArrayByteLength, + ); + } else { + Reflect.deleteProperty(Uint8Array.prototype, "byteLength"); + } + if (original.uint8ArrayLength) { + Object.defineProperty( + Uint8Array.prototype, + "length", + original.uint8ArrayLength, + ); + } else { + Reflect.deleteProperty(Uint8Array.prototype, "length"); + } + } + + assertEquals(snapshot?.imports?.package, "https://example.com/package.ts"); + assertEquals(Object.isFrozen(snapshot), true); + assertEquals(Object.isFrozen(snapshot?.imports), true); + assertEquals(typeof fingerprint, "string"); + assertEquals(fingerprint?.length, 64); + assertEquals(pluginIdentity?.cacheable, true); + if (!pluginIdentity?.cacheable) { + throw new Error("Expected a cacheable plugin identity"); + } + assertEquals(pluginIdentity.identity.length, 1); + assertEquals(typeof pipelineIdentity, "string"); + assertEquals(pipelineIdentity?.length, 64); + assertEquals(invalidMapError instanceof original.typeError, true); + assertEquals( + oversizedPluginListError instanceof original.rangeError, + true, + ); + }); +}); diff --git a/src/transforms/pipeline/cache-identity.ts b/src/transforms/pipeline/cache-identity.ts new file mode 100644 index 0000000000..6fe24e4f10 --- /dev/null +++ b/src/transforms/pipeline/cache-identity.ts @@ -0,0 +1,538 @@ +import { computeHash } from "#veryfront/utils/hash-utils.ts"; +import { computeConfigHash } from "#veryfront/cache/config-hash.ts"; +import { fingerprintImportMap } from "../esm/http-cache-helpers.ts"; +import type { ImportMapConfig } from "#veryfront/modules/import-map/types.ts"; +import type { TransformPlugin } from "./types.ts"; + +const MAX_IMPORT_MAP_ENTRIES = 20_000; +const MAX_IDENTITY_STRING_BYTES = 64 * 1024; +const MAX_IMPORT_MAP_IDENTITY_BYTES = 8 * 1024 * 1024; +const MAX_PLUGIN_IDENTITY_BYTES = 4 * 1024; +const MAX_CUSTOM_PLUGINS = 1_000; +const MAX_TRANSFORM_STAGE_MAGNITUDE = 1_000_000; + +// Transform identities are derived after project code may have run in the +// shared realm. Keep descriptor inspection, freezing, and bounded string +// handling independent from later primordial replacement. +const ArrayIsArray = Array.isArray; +const ArrayPrototypePush = Array.prototype.push; +const IntrinsicTextEncoder = TextEncoder; +const IntrinsicUint8Array = Uint8Array; +const IntrinsicRangeError = RangeError; +const IntrinsicTypeError = TypeError; +const JSONStringify = JSON.stringify; +const MathAbs = Math.abs; +const NumberIsFinite = Number.isFinite; +const NumberIsSafeInteger = Number.isSafeInteger; +const ObjectCreate = Object.create; +const ObjectFreeze = Object.freeze; +const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const ObjectGetPrototypeOf = Object.getPrototypeOf; +const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; +const ObjectPrototype = Object.prototype; +const ReflectApply = Reflect.apply; +const ReflectOwnKeys = Reflect.ownKeys; +const RegExpPrototypeTest = RegExp.prototype.test; +const StringPrototypeTrim = String.prototype.trim; +const TextEncoderPrototypeEncode = TextEncoder.prototype.encode; +const controlCharacterPattern = /\p{Cc}/u; +const encoder = new IntrinsicTextEncoder(); +const TypedArrayPrototype = ObjectGetPrototypeOf(IntrinsicUint8Array.prototype); +const TypedArrayByteLengthGetter = ObjectGetOwnPropertyDescriptor( + TypedArrayPrototype, + "byteLength", +)!.get!; + +function encodedByteLength(value: string): number { + const bytes = ReflectApply( + TextEncoderPrototypeEncode, + encoder, + [value], + ) as Uint8Array; + return ReflectApply(TypedArrayByteLengthGetter, bytes, []) as number; +} + +function hasOwn(object: object, key: PropertyKey): boolean { + return ReflectApply(ObjectPrototypeHasOwnProperty, object, [key]) as boolean; +} + +/** + * Transform stages are ordered numeric coordinates, not enum membership. + * Built-in and custom plugins deliberately use fractional coordinates to run + * between the public enum anchors, so every finite bounded number is valid. + */ +function isValidTransformStage(value: unknown): value is number { + return typeof value === "number" && NumberIsFinite(value) && + MathAbs(value) <= MAX_TRANSFORM_STAGE_MAGNITUDE; +} + +interface ImportMapBudget { + entries: number; + bytes: number; +} + +function readOwnDataProperty(value: object, key: PropertyKey, label: string): unknown { + const descriptor = ObjectGetOwnPropertyDescriptor(value, key); + if (!descriptor) return undefined; + if (!hasOwn(descriptor, "value")) { + throw new IntrinsicTypeError(`${label} cannot contain accessor properties`); + } + return descriptor.value; +} + +function readPluginDataProperty(value: object, key: PropertyKey, label: string): unknown { + let current: object | null = value; + while (current !== null && current !== ObjectPrototype) { + const descriptor = ObjectGetOwnPropertyDescriptor(current, key); + if (descriptor) { + if (!hasOwn(descriptor, "value")) { + throw new IntrinsicTypeError(`${label} cannot contain accessor properties`); + } + return descriptor.value; + } + current = ObjectGetPrototypeOf(current); + } + return undefined; +} + +function countIdentityString( + value: string, + budget: ImportMapBudget, + label: string, + maxBytes = MAX_IDENTITY_STRING_BYTES, +): string { + const bytes = encodedByteLength(value); + if (bytes > maxBytes) throw new IntrinsicTypeError(`${label} is too large`); + budget.bytes += bytes; + if (budget.bytes > MAX_IMPORT_MAP_IDENTITY_BYTES) { + throw new IntrinsicTypeError("Import map cache identity exceeds its byte limit"); + } + return value; +} + +function snapshotStringRecord( + value: unknown, + label: string, + budget: ImportMapBudget, +): Readonly> { + if (value === undefined) { + return ObjectFreeze(ObjectCreate(null) as Record); + } + if (value === null || typeof value !== "object" || ArrayIsArray(value)) { + throw new IntrinsicTypeError(`${label} must be a plain object`); + } + const prototype = ObjectGetPrototypeOf(value); + if (prototype !== ObjectPrototype && prototype !== null) { + throw new IntrinsicTypeError(`${label} must be a plain object`); + } + + const snapshot = ObjectCreate(null) as Record; + const keys = ReflectOwnKeys(value); + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + if (typeof key !== "string") { + throw new IntrinsicTypeError(`${label} cannot contain symbol keys`); + } + const descriptor = ObjectGetOwnPropertyDescriptor(value, key); + if (!descriptor) continue; + if (!hasOwn(descriptor, "value")) { + throw new IntrinsicTypeError(`${label} cannot contain accessor properties`); + } + if (!descriptor.enumerable) continue; + + budget.entries++; + if (budget.entries > MAX_IMPORT_MAP_ENTRIES) { + throw new IntrinsicTypeError("Import map cache identity contains too many entries"); + } + countIdentityString(key, budget, `${label} key`); + if (typeof descriptor.value !== "string") { + throw new IntrinsicTypeError(`${label}.${key} must be a string`); + } + snapshot[key] = countIdentityString(descriptor.value, budget, `${label}.${key}`); + } + return ObjectFreeze(snapshot); +} + +/** + * Take a descriptor-only immutable snapshot before an import map is shared by + * cache identity computation and transform stages. This prevents later caller + * mutation (or getters with side effects) from making those two views diverge. + */ +export function snapshotImportMap(value: unknown): ImportMapConfig { + if (value === null || typeof value !== "object" || ArrayIsArray(value)) { + throw new IntrinsicTypeError("Import map must be a plain object"); + } + const prototype = ObjectGetPrototypeOf(value); + if (prototype !== ObjectPrototype && prototype !== null) { + throw new IntrinsicTypeError("Import map must be a plain object"); + } + + const keys = ReflectOwnKeys(value); + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + if (typeof key !== "string") { + throw new IntrinsicTypeError("Import map cannot contain symbol keys"); + } + if (key !== "imports" && key !== "scopes") { + const descriptor = ObjectGetOwnPropertyDescriptor(value, key); + if (descriptor?.enumerable) { + throw new IntrinsicTypeError(`Unknown import map field: ${key}`); + } + } + } + + const budget: ImportMapBudget = { entries: 0, bytes: 0 }; + const imports = snapshotStringRecord( + readOwnDataProperty(value, "imports", "Import map"), + "Import map imports", + budget, + ); + const rawScopes = readOwnDataProperty(value, "scopes", "Import map"); + const scopes = ObjectCreate(null) as Record>>; + + if (rawScopes !== undefined) { + if (rawScopes === null || typeof rawScopes !== "object" || ArrayIsArray(rawScopes)) { + throw new IntrinsicTypeError("Import map scopes must be a plain object"); + } + const scopesPrototype = ObjectGetPrototypeOf(rawScopes); + if (scopesPrototype !== ObjectPrototype && scopesPrototype !== null) { + throw new IntrinsicTypeError("Import map scopes must be a plain object"); + } + const scopeKeys = ReflectOwnKeys(rawScopes); + for (let index = 0; index < scopeKeys.length; index++) { + const scope = scopeKeys[index]; + if (typeof scope !== "string") { + throw new IntrinsicTypeError("Import map scopes cannot contain symbol keys"); + } + const descriptor = ObjectGetOwnPropertyDescriptor(rawScopes, scope); + if (!descriptor) continue; + if (!hasOwn(descriptor, "value")) { + throw new IntrinsicTypeError("Import map scopes cannot contain accessor properties"); + } + if (!descriptor.enumerable) continue; + budget.entries++; + if (budget.entries > MAX_IMPORT_MAP_ENTRIES) { + throw new IntrinsicTypeError("Import map cache identity contains too many entries"); + } + countIdentityString(scope, budget, "Import map scope"); + scopes[scope] = snapshotStringRecord( + descriptor.value, + `Import map scope ${scope}`, + budget, + ); + } + } + + return ObjectFreeze({ + imports, + scopes: ObjectFreeze(scopes), + }); +} + +export function fingerprintPipelineImportMap(importMap: ImportMapConfig): Promise { + return fingerprintImportMap(importMap); +} + +export type CustomPluginCacheIdentity = + | { + cacheable: true; + identity: ReadonlyArray; + plugins: ReadonlyArray; + } + | { cacheable: false; reason: string; plugins: ReadonlyArray }; + +/** Require explicit versioned identities for caller-supplied executable code. */ +export function getCustomPluginCacheIdentity( + plugins: readonly TransformPlugin[] | undefined, +): CustomPluginCacheIdentity { + if (plugins === undefined) { + return { + cacheable: true, + identity: ObjectFreeze([]), + plugins: ObjectFreeze([]), + }; + } + if (!ArrayIsArray(plugins)) { + throw new IntrinsicTypeError("Transform pipeline plugins must be an array"); + } + const pluginCount = readArrayLength(plugins, "Transform pipeline plugins"); + if (pluginCount === 0) { + return { + cacheable: true, + identity: ObjectFreeze([]), + plugins: ObjectFreeze([]), + }; + } + if (pluginCount > MAX_CUSTOM_PLUGINS) { + throw new IntrinsicRangeError( + `Transform pipeline cannot contain more than ${MAX_CUSTOM_PLUGINS} plugins`, + ); + } + + const identity: Array = []; + const pluginSnapshot: TransformPlugin[] = []; + let uncacheableReason: string | undefined; + for (let index = 0; index < pluginCount; index++) { + const plugin = readArrayElement(plugins, index, "Transform pipeline plugins"); + if (plugin === null || typeof plugin !== "object") { + throw new IntrinsicTypeError(`Transform plugin at index ${index} must be an object`); + } + const name = readPluginDataProperty(plugin, "name", `Transform plugin ${index}`); + const stage = readPluginDataProperty(plugin, "stage", `Transform plugin ${index}`); + const cacheIdentity = readPluginDataProperty( + plugin, + "cacheIdentity", + `Transform plugin ${index}`, + ); + const condition = readPluginDataProperty(plugin, "condition", `Transform plugin ${index}`); + const transform = readPluginDataProperty(plugin, "transform", `Transform plugin ${index}`); + if ( + typeof name !== "string" || name.length === 0 || name.length > 256 || + (ReflectApply(StringPrototypeTrim, name, []) as string) !== name || + (ReflectApply(RegExpPrototypeTest, controlCharacterPattern, [name]) as boolean) + ) { + throw new IntrinsicTypeError(`Transform plugin at index ${index} has an invalid name`); + } + if (!isValidTransformStage(stage)) { + throw new IntrinsicTypeError(`Transform plugin ${name} has an invalid stage`); + } + if (condition !== undefined && typeof condition !== "function") { + throw new IntrinsicTypeError(`Transform plugin ${name} has an invalid condition`); + } + if (typeof transform !== "function") { + throw new IntrinsicTypeError(`Transform plugin ${name} has an invalid transform`); + } + + const exactPlugin = ObjectCreate(null) as TransformPlugin; + exactPlugin.name = name; + exactPlugin.stage = stage; + if (cacheIdentity !== undefined) exactPlugin.cacheIdentity = cacheIdentity as string; + if (condition !== undefined) exactPlugin.condition = condition as TransformPlugin["condition"]; + exactPlugin.transform = transform as TransformPlugin["transform"]; + ReflectApply(ArrayPrototypePush, pluginSnapshot, [ObjectFreeze(exactPlugin)]); + + if (cacheIdentity === undefined) { + uncacheableReason ??= `custom transform plugin ${name} has no cacheIdentity`; + continue; + } + if ( + typeof cacheIdentity !== "string" || cacheIdentity.length === 0 || + encodedByteLength(cacheIdentity) > MAX_PLUGIN_IDENTITY_BYTES + ) { + throw new IntrinsicTypeError(`Transform plugin ${name} has an invalid cacheIdentity`); + } + ReflectApply( + ArrayPrototypePush, + identity, + [ObjectFreeze([index, name, stage, cacheIdentity] as const)], + ); + } + const exactPlugins = ObjectFreeze(pluginSnapshot); + if (uncacheableReason !== undefined) { + return { cacheable: false, reason: uncacheableReason, plugins: exactPlugins }; + } + return { + cacheable: true, + identity: ObjectFreeze(identity), + plugins: exactPlugins, + }; +} + +function boundedOption(value: unknown, label: string): string | null { + if (value === undefined) return null; + if (typeof value !== "string") { + throw new IntrinsicTypeError(`${label} must be a string`); + } + if ( + encodedByteLength(value) > MAX_IDENTITY_STRING_BYTES + ) { + throw new IntrinsicTypeError(`${label} is too large for transform cache identity`); + } + return value; +} + +function boundedRequiredOption(value: unknown, label: string): string { + const bounded = boundedOption(value, label); + if (bounded === null) throw new IntrinsicTypeError(`${label} must be a string`); + return bounded; +} + +function readArrayLength(value: readonly unknown[], label: string): number { + const length = readOwnDataProperty(value, "length", label); + if ( + typeof length !== "number" || !NumberIsSafeInteger(length) || length < 0 + ) { + throw new IntrinsicTypeError(`${label} has an invalid length`); + } + return length; +} + +function readArrayElement( + value: readonly unknown[], + index: number, + label: string, +): unknown { + const descriptor = ObjectGetOwnPropertyDescriptor(value, index); + if (!descriptor || !hasOwn(descriptor, "value")) { + throw new IntrinsicTypeError(`${label} must contain own data elements`); + } + return descriptor.value; +} + +function encodeIdentityPrimitive(value: string | number | boolean | null): string { + return JSONStringify(value) as string; +} + +function encodeCustomPluginIdentities( + plugins: ReadonlyArray, +): string { + if (!ArrayIsArray(plugins)) { + throw new IntrinsicTypeError("Transform pipeline custom plugin identity must be an array"); + } + const length = readArrayLength(plugins, "Transform pipeline custom plugin identity"); + if (length > MAX_CUSTOM_PLUGINS) { + throw new IntrinsicRangeError( + `Transform pipeline cache identity cannot contain more than ${MAX_CUSTOM_PLUGINS} plugins`, + ); + } + + let encoded = `${encodeIdentityPrimitive(length)};`; + for (let index = 0; index < length; index++) { + const tuple = readArrayElement( + plugins, + index, + `Transform pipeline custom plugin identity ${index}`, + ); + if (!ArrayIsArray(tuple) || readArrayLength(tuple, `Custom plugin identity ${index}`) !== 4) { + throw new IntrinsicTypeError(`Custom plugin identity ${index} must be a four-item tuple`); + } + const pluginIndex = readArrayElement(tuple, 0, `Custom plugin identity ${index}`); + const name = readArrayElement(tuple, 1, `Custom plugin identity ${index}`); + const stage = readArrayElement(tuple, 2, `Custom plugin identity ${index}`); + const cacheIdentity = readArrayElement(tuple, 3, `Custom plugin identity ${index}`); + if (pluginIndex !== index) { + throw new IntrinsicTypeError(`Custom plugin identity ${index} has an invalid index`); + } + if ( + typeof name !== "string" || name.length === 0 || name.length > 256 || + (ReflectApply(StringPrototypeTrim, name, []) as string) !== name || + (ReflectApply(RegExpPrototypeTest, controlCharacterPattern, [name]) as boolean) + ) { + throw new IntrinsicTypeError(`Custom plugin identity ${index} has an invalid name`); + } + if (!isValidTransformStage(stage)) { + throw new IntrinsicTypeError(`Custom plugin identity ${index} has an invalid stage`); + } + if ( + typeof cacheIdentity !== "string" || cacheIdentity.length === 0 || + encodedByteLength(cacheIdentity) > MAX_PLUGIN_IDENTITY_BYTES + ) { + throw new IntrinsicTypeError( + `Custom plugin identity ${index} has an invalid cache identity`, + ); + } + encoded += `${encodeIdentityPrimitive(pluginIndex)},${encodeIdentityPrimitive(name)},`; + encoded += `${encodeIdentityPrimitive(stage)},${encodeIdentityPrimitive(cacheIdentity)};`; + } + return encoded; +} + +export interface PipelineConfigIdentityInput { + reactVersion: string; + jsxImportSource: string; + studioEmbed: boolean; + dev: boolean; + ssr: boolean; + projectDir: string; + moduleServerUrl?: string; + moduleServerOrigin?: string; + vendorBundleHash?: string; + apiBaseUrl?: string; + importMapFingerprint?: string; + dependencyPinningCacheKey?: string; + customPlugins: ReadonlyArray; +} + +/** Hash every known output-affecting pipeline input using full SHA-256. */ +export async function computePipelineConfigIdentity( + input: PipelineConfigIdentityInput, +): Promise { + const reactVersion = boundedRequiredOption( + readOwnDataProperty(input, "reactVersion", "Transform pipeline identity"), + "React version", + ); + const jsxImportSource = boundedRequiredOption( + readOwnDataProperty(input, "jsxImportSource", "Transform pipeline identity"), + "JSX import source", + ); + const projectDir = boundedRequiredOption( + readOwnDataProperty(input, "projectDir", "Transform pipeline identity"), + "Project directory", + ); + const studioEmbed = readOwnDataProperty( + input, + "studioEmbed", + "Transform pipeline identity", + ); + const dev = readOwnDataProperty(input, "dev", "Transform pipeline identity"); + const ssr = readOwnDataProperty(input, "ssr", "Transform pipeline identity"); + if ( + typeof studioEmbed !== "boolean" || typeof dev !== "boolean" || + typeof ssr !== "boolean" + ) { + throw new IntrinsicTypeError("Transform pipeline mode identity fields must be booleans"); + } + const customPlugins = encodeCustomPluginIdentities( + readOwnDataProperty( + input, + "customPlugins", + "Transform pipeline identity", + ) as ReadonlyArray, + ); + const baseIdentity = await computeConfigHash({ + reactVersion, + jsxImportSource, + studioEmbed, + dev, + }); + let identity = "veryfront:transform-pipeline:v4;"; + identity += `base=${encodeIdentityPrimitive(baseIdentity)};`; + identity += `ssr=${encodeIdentityPrimitive(ssr)};`; + identity += `project=${encodeIdentityPrimitive(projectDir)};`; + const moduleServerUrl = boundedOption( + readOwnDataProperty(input, "moduleServerUrl", "Transform pipeline identity"), + "Module server URL", + ); + const moduleServerOrigin = boundedOption( + readOwnDataProperty(input, "moduleServerOrigin", "Transform pipeline identity"), + "Module server origin", + ); + const vendorBundleHash = boundedOption( + readOwnDataProperty(input, "vendorBundleHash", "Transform pipeline identity"), + "Vendor bundle hash", + ); + const apiBaseUrl = boundedOption( + readOwnDataProperty(input, "apiBaseUrl", "Transform pipeline identity"), + "API base URL", + ); + const importMapFingerprint = boundedOption( + readOwnDataProperty(input, "importMapFingerprint", "Transform pipeline identity"), + "Import map fingerprint", + ); + const dependencyPinningCacheKey = boundedOption( + readOwnDataProperty( + input, + "dependencyPinningCacheKey", + "Transform pipeline identity", + ), + "Dependency pinning cache key", + ); + identity += `module-url=${encodeIdentityPrimitive(moduleServerUrl)};`; + identity += `module-origin=${encodeIdentityPrimitive(moduleServerOrigin)};`; + identity += `vendor=${encodeIdentityPrimitive(vendorBundleHash)};`; + identity += `api=${encodeIdentityPrimitive(apiBaseUrl)};`; + identity += `import-map=${encodeIdentityPrimitive(importMapFingerprint)};`; + identity += `dependency-pins=${encodeIdentityPrimitive(dependencyPinningCacheKey)};`; + identity += `plugins=${customPlugins}`; + return computeHash(identity); +} diff --git a/src/transforms/pipeline/index.test.ts b/src/transforms/pipeline/index.test.ts index ee212af0e4..032b0eb1d9 100644 --- a/src/transforms/pipeline/index.test.ts +++ b/src/transforms/pipeline/index.test.ts @@ -11,7 +11,7 @@ import { } from "#veryfront/testing/deno-compat.ts"; import { join } from "#veryfront/compat/path"; import * as esbuild from "veryfront/extensions/bundler"; -import { runPipeline, transformToESM } from "./index.ts"; +import { runPipeline, TransformStage, transformToESM } from "./index.ts"; import { getHostEnv, setEnv } from "#veryfront/platform/compat/process.ts"; import { DEPENDENCY_PINNING_ENV_FLAG } from "../../release-assets/constants.ts"; import { @@ -136,6 +136,267 @@ export default function App() { return dep; }`; } }); + it("invalidates cached transforms when the project import map changes", async () => { + const projectDir = await makeTempDir({ prefix: "vf-pipeline-import-map-" }); + const mainFile = join(projectDir, "main.ts"); + const denoJsonPath = join(projectDir, "deno.json"); + const source = `import value from "project-alias"; export default value;`; + const options = { + projectId: "import-map-cache-project", + dev: false, + ssr: true, + }; + + try { + destroyTransformCache(); + await writeTextFile(mainFile, source); + await writeTextFile( + denoJsonPath, + JSON.stringify({ imports: { "project-alias": "/project-v1.js" } }), + ); + const first = await runPipeline(source, mainFile, projectDir, options); + + await writeTextFile( + denoJsonPath, + JSON.stringify({ imports: { "project-alias": "/project-v2.js" } }), + ); + const second = await runPipeline(source, mainFile, projectDir, options); + + assertEquals(first.code.includes("/project-v1.js"), true); + assertEquals(second.cached, false); + assertEquals(second.code.includes("/project-v2.js"), true); + assertEquals(second.code.includes("/project-v1.js"), false); + } finally { + destroyTransformCache(); + await remove(projectDir, { recursive: true }); + } + }); + + it("uses one preloaded SSR import-map snapshot for cache identity and stages", async () => { + const projectDir = await makeTempDir({ prefix: "vf-pipeline-preloaded-map-" }); + const mainFile = join(projectDir, "main.ts"); + const denoJsonPath = join(projectDir, "deno.json"); + const source = `import value from "project-alias"; export default value;`; + const options = { + projectId: "preloaded-import-map-cache-project", + dev: false, + ssr: true, + preloadedImportMap: { + imports: { "project-alias": "/preloaded-v1.js" }, + scopes: {}, + }, + }; + + try { + destroyTransformCache(); + await writeTextFile(mainFile, source); + await writeTextFile( + denoJsonPath, + JSON.stringify({ imports: { "project-alias": "/disk-v2.js" } }), + ); + + const first = await runPipeline(source, mainFile, projectDir, options); + const second = await runPipeline(source, mainFile, projectDir, options); + const changed = await runPipeline(source, mainFile, projectDir, { + ...options, + preloadedImportMap: { + imports: { "project-alias": "/preloaded-v2.js" }, + scopes: {}, + }, + }); + + assertEquals(first.cached, false); + assertEquals(first.code.includes("/preloaded-v1.js"), true); + assertEquals(first.code.includes("/disk-v2.js"), false); + assertEquals(second.cached, true); + assertEquals(second.code.includes("/preloaded-v1.js"), true); + assertEquals(changed.cached, false); + assertEquals(changed.code.includes("/preloaded-v2.js"), true); + assertEquals(changed.code.includes("/preloaded-v1.js"), false); + } finally { + destroyTransformCache(); + await remove(projectDir, { recursive: true }); + } + }); + + it("isolates identified custom plugin output and disables caching without an identity", async () => { + const projectDir = await makeTempDir({ prefix: "vf-pipeline-custom-plugin-" }); + const mainFile = join(projectDir, "main.ts"); + const source = "export const value = 1;"; + const options = { + projectId: "custom-plugin-cache-project", + dev: false, + ssr: false, + }; + + try { + destroyTransformCache(); + const first = await runPipeline(source, mainFile, projectDir, options, { + plugins: [{ + name: "custom-output", + stage: TransformStage.FINALIZE, + cacheIdentity: "custom-output@1", + transform: (ctx) => `${ctx.code}\n/* custom-v1 */`, + }], + }); + const changed = await runPipeline(source, mainFile, projectDir, options, { + plugins: [{ + name: "custom-output", + stage: TransformStage.FINALIZE, + cacheIdentity: "custom-output@2", + transform: (ctx) => `${ctx.code}\n/* custom-v2 */`, + }], + }); + + assertEquals(first.code.includes("custom-v1"), true); + assertEquals(changed.cached, false); + assertEquals(changed.code.includes("custom-v2"), true); + assertEquals(changed.code.includes("custom-v1"), false); + + destroyTransformCache(); + let calls = 0; + const unidentified = { + plugins: [{ + name: "unidentified-output", + stage: TransformStage.FINALIZE, + transform: (ctx: { code: string }) => { + calls++; + return `${ctx.code}\n/* unidentified-${calls} */`; + }, + }], + }; + const uncachedFirst = await runPipeline( + source, + mainFile, + projectDir, + options, + unidentified, + ); + const uncachedSecond = await runPipeline( + source, + mainFile, + projectDir, + options, + unidentified, + ); + + assertEquals(uncachedFirst.cached, false); + assertEquals(uncachedSecond.cached, false); + assertEquals(calls, 2); + assertEquals(uncachedSecond.code.includes("unidentified-2"), true); + } finally { + destroyTransformCache(); + await remove(projectDir, { recursive: true }); + } + }); + + it("binds custom plugin execution to its cache-identity snapshot", async () => { + const projectDir = await makeTempDir({ prefix: "vf-pipeline-plugin-snapshot-" }); + const mainFile = join(projectDir, "main.ts"); + const dependencyFile = join(projectDir, "dependency.ts"); + const source = `import "./dependency.ts"; export const value = 1;`; + const plugin = { + name: "mutable-output", + stage: TransformStage.FINALIZE, + cacheIdentity: "mutable-output@1", + transform: (ctx: { code: string }) => `${ctx.code}\n/* snapshot-v1 */`, + }; + + try { + destroyTransformCache(); + await writeTextFile(mainFile, source); + await writeTextFile(dependencyFile, "export const dependency = 1;"); + + const result = await runPipeline( + source, + mainFile, + projectDir, + { + projectId: "plugin-snapshot-cache-project", + dev: false, + ssr: false, + readFile: async (path) => { + plugin.transform = (ctx) => `${ctx.code}\n/* mutated-v2 */`; + return await readTextFile(path); + }, + }, + { plugins: [plugin] }, + ); + + assertEquals(result.code.includes("snapshot-v1"), true); + assertEquals(result.code.includes("mutated-v2"), false); + } finally { + destroyTransformCache(); + await remove(projectDir, { recursive: true }); + } + }); + + it("uses captured array operations for custom plugin execution", async () => { + const projectDir = await makeTempDir({ prefix: "vf-pipeline-plugin-primordials-" }); + const mainFile = join(projectDir, "main.ts"); + const source = "export const value = 1;"; + const originalArrayIterator = Array.prototype[Symbol.iterator]; + const originalArraySort = Array.prototype.sort; + const isSentinelPipeline = (values: unknown[]): boolean => { + for (let index = 0; index < values.length; index++) { + const value = values[index] as { name?: unknown } | undefined; + if (value?.name === "sentinel-early" || value?.name === "sentinel-late") return true; + } + return false; + }; + + try { + destroyTransformCache(); + await writeTextFile(mainFile, source); + Reflect.set(Array.prototype, Symbol.iterator, function (this: unknown[]) { + const values = this as unknown[]; + if (isSentinelPipeline(values)) { + return { next: () => ({ done: true, value: undefined }) }; + } + return Reflect.apply(originalArrayIterator, values, []); + }); + Reflect.set(Array.prototype, "sort", function ( + this: unknown[], + compare?: (left: unknown, right: unknown) => number, + ) { + if (isSentinelPipeline(this)) return this; + return Reflect.apply(originalArraySort, this, [compare]); + }); + + const result = await runPipeline( + source, + mainFile, + projectDir, + { projectId: "plugin-primordial-project", dev: false, ssr: false }, + { + plugins: [{ + name: "sentinel-late", + stage: TransformStage.FINALIZE + 0.75, + cacheIdentity: "sentinel-late@1", + transform: (ctx) => `${ctx.code}\n/* sentinel-late */`, + }, { + name: "sentinel-early", + stage: TransformStage.FINALIZE + 0.25, + cacheIdentity: "sentinel-early@1", + transform: (ctx) => `${ctx.code}\n/* sentinel-early */`, + }], + }, + ); + + assertEquals(result.code.includes("sentinel-early"), true); + assertEquals(result.code.includes("sentinel-late"), true); + assertEquals( + result.code.indexOf("sentinel-early") < result.code.indexOf("sentinel-late"), + true, + ); + } finally { + Reflect.set(Array.prototype, Symbol.iterator, originalArrayIterator); + Reflect.set(Array.prototype, "sort", originalArraySort); + destroyTransformCache(); + await remove(projectDir, { recursive: true }); + } + }); + it("replays cached unresolved dependencies through TTL and current-snapshot gates", async () => { const projectDir = await makeTempDir({ prefix: "vf-pipeline-retry-replay-" }); const mainFile = join(projectDir, "main.ts"); diff --git a/src/transforms/pipeline/index.ts b/src/transforms/pipeline/index.ts index f17d840607..eda262e381 100644 --- a/src/transforms/pipeline/index.ts +++ b/src/transforms/pipeline/index.ts @@ -12,7 +12,6 @@ import { import { rendererLogger } from "#veryfront/utils"; import { createTransformContext, formatTimingLog, recordStageTiming } from "./context.ts"; import { withSpan } from "#veryfront/observability/tracing/otlp-setup.ts"; -import { computeConfigHash } from "#veryfront/cache/config-hash.ts"; import { computeDepsHash } from "#veryfront/cache/dependency-graph.ts"; import type { PipelineConfig, @@ -43,6 +42,18 @@ import { validateDependencyResolutionObservations, } from "../import-rewriter/dependency-resolution.ts"; import { getDependencyResolutionObservations } from "./stages/resolve-imports.ts"; +import { loadImportMap, preloadImportMap } from "#veryfront/modules/import-map/index.ts"; +import type { ImportMapConfig } from "#veryfront/modules/import-map/types.ts"; +import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; +import { + computePipelineConfigIdentity, + fingerprintPipelineImportMap, + getCustomPluginCacheIdentity, +} from "./cache-identity.ts"; +import { + primordialArrayPush, + primordialArraySort, +} from "#veryfront/platform/compat/primordials/array.ts"; const SSR_PIPELINE: TransformPlugin[] = [ parsePlugin, @@ -188,6 +199,9 @@ export function runPipeline( "transform.pipeline", async () => { const transformStart = performance.now(); + // Snapshot executable custom-plugin fields before the first await. The + // same immutable view must supply both cache identity and execution. + const pluginCacheIdentity = getCustomPluginCacheIdentity(config?.plugins); const dependencySnapshot = await resolveDependencyPinningSnapshot( options.dependencyPinningSource ?? projectDir, @@ -208,35 +222,50 @@ export function runPipeline( ctx.debug = config?.debug ?? false; ctx.onProgress?.({ phase: "pipeline:context", filePath }); - const configHash = await computeConfigHash({ - reactVersion: ctx.reactVersion, - jsxImportSource: ctx.jsxImportSource, - moduleServerUrl: ctx.moduleServerUrl, - moduleServerOrigin: ctx.moduleServerOrigin, - vendorBundleHash: ctx.vendorBundleHash, - apiBaseUrl: ctx.apiBaseUrl, - studioEmbed: ctx.studioEmbed, - dev: ctx.dev, - dependencyPinningCacheKey, - }); - - const depsHash = await computeDepsHashSafe( - filePath, - projectDir, - effectiveOptions.readFile, - effectiveOptions.dependencyHashCache, - ); + let importMapFingerprint: string | undefined; + if (effectiveOptions.ssr) { + const importMap = await resolvePipelineImportMap(projectDir, effectiveOptions); + importMapFingerprint = await fingerprintPipelineImportMap(importMap); + ctx.metadata.set("importMap", importMap); + ctx.metadata.set("importMapFingerprint", importMapFingerprint); + } - const cacheKey = generateCacheKey( - filePath, - ctx.contentHash, - effectiveOptions.ssr ?? false, - effectiveOptions.studioEmbed ?? false, - { depsHash, configHash, projectId: effectiveOptions.projectId }, - ); + let cacheKey: string | undefined; + if (pluginCacheIdentity.cacheable) { + const [configHash, depsHash] = await Promise.all([ + computePipelineConfigIdentity({ + reactVersion: ctx.reactVersion, + jsxImportSource: ctx.jsxImportSource, + moduleServerUrl: ctx.moduleServerUrl, + moduleServerOrigin: ctx.moduleServerOrigin, + vendorBundleHash: ctx.vendorBundleHash, + apiBaseUrl: ctx.apiBaseUrl, + studioEmbed: ctx.studioEmbed ?? false, + dev: ctx.dev, + ssr: effectiveOptions.ssr ?? false, + projectDir, + importMapFingerprint, + dependencyPinningCacheKey, + customPlugins: pluginCacheIdentity.identity, + }), + computeDepsHashSafe( + filePath, + projectDir, + effectiveOptions.readFile, + effectiveOptions.dependencyHashCache, + ), + ]); + cacheKey = generateCacheKey( + filePath, + ctx.contentHash, + effectiveOptions.ssr ?? false, + effectiveOptions.studioEmbed ?? false, + { depsHash, configHash, projectId: effectiveOptions.projectId }, + ); + } - const cached = await getCachedTransformAsync(cacheKey); - if (cached) { + const cached = cacheKey ? await getCachedTransformAsync(cacheKey) : undefined; + if (cached && cacheKey) { const dependencyResolutionObservations = validateCachedDependencyResolutionObservations( cached, ctx, @@ -302,11 +331,23 @@ export function runPipeline( } const basePipeline = effectiveOptions.ssr ? SSR_PIPELINE : BROWSER_PIPELINE; - const pipeline = config?.plugins - ? [...basePipeline, ...config.plugins].sort((a, b) => a.stage - b.stage) - : basePipeline; + let pipeline: readonly TransformPlugin[] = basePipeline; + if (pluginCacheIdentity.plugins.length > 0) { + const sortedPipeline: TransformPlugin[] = []; + for (let index = 0; index < basePipeline.length; index++) { + primordialArrayPush(sortedPipeline, basePipeline[index]); + } + for (let index = 0; index < pluginCacheIdentity.plugins.length; index++) { + primordialArrayPush(sortedPipeline, pluginCacheIdentity.plugins[index]); + } + pipeline = primordialArraySort( + sortedPipeline, + (left, right) => left.stage - right.stage, + ); + } - for (const plugin of pipeline) { + for (let index = 0; index < pipeline.length; index++) { + const plugin = pipeline[index]!; if (plugin.condition?.(ctx) === false) continue; const stageStart = performance.now(); @@ -333,19 +374,21 @@ export function runPipeline( // Store the bundleManifestId from ssrHttpCachePlugin for future cache validation const bundleManifestId = ctx.metadata.get("bundleManifestId") as string | undefined; const dependencyResolutionObservations = getDependencyResolutionObservations(ctx); - setCachedTransformAsync( - cacheKey, - ctx.code, - ctx.contentHash, - undefined, - bundleManifestId, - dependencyResolutionObservations, - ) - .catch( - (error) => { - logger.debug("Failed to cache transform", { error }); - }, - ); + if (cacheKey) { + setCachedTransformAsync( + cacheKey, + ctx.code, + ctx.contentHash, + undefined, + bundleManifestId, + dependencyResolutionObservations, + ) + .catch( + (error) => { + logger.debug("Failed to cache transform", { error }); + }, + ); + } const totalMs = performance.now() - transformStart; @@ -399,14 +442,34 @@ export async function transformToESM( ): Promise { if (filePath.endsWith(".css") || filePath.endsWith(".json")) return source; - const enrichedOptions: TransformOptions = options.readFile - ? options - : { ...options, readFile: buildReadFile(adapter, projectDir) }; + const importMapAdapter = options.importMapAdapter ?? + (adapter ? adapter as RuntimeAdapter : undefined); + const enrichedOptions: TransformOptions = { + ...options, + ...(options.readFile ? {} : { readFile: buildReadFile(adapter, projectDir) }), + ...(importMapAdapter ? { importMapAdapter } : {}), + }; const { code } = await runPipeline(source, filePath, projectDir, enrichedOptions); return code; } +async function resolvePipelineImportMap( + projectDir: string, + options: TransformOptions, +): Promise { + if (options.preloadedImportMap) return options.preloadedImportMap; + if (options.importMapAdapter) { + return await preloadImportMap( + projectDir, + options.importMapAdapter, + options.projectId, + options.importMapPreloadContext, + ); + } + return await loadImportMap(projectDir); +} + /** Extract readFile from adapter if available, for dependency hash computation. */ function extractReadFile(adapter: unknown): ((path: string) => Promise) | undefined { const a = adapter as { fs?: { readFile?: (path: string) => Promise } } | null; diff --git a/src/transforms/pipeline/stages/ssr-vf-modules/constants.ts b/src/transforms/pipeline/stages/ssr-vf-modules/constants.ts index 5aec0d4a52..52565f4590 100644 --- a/src/transforms/pipeline/stages/ssr-vf-modules/constants.ts +++ b/src/transforms/pipeline/stages/ssr-vf-modules/constants.ts @@ -5,6 +5,7 @@ import { join } from "#veryfront/compat/path/index.ts"; import { createFileSystem } from "#veryfront/platform/compat/fs.ts"; import type { TransformProgressListener } from "#veryfront/transforms/progress.ts"; +import type { ImportMapConfig } from "#veryfront/modules/import-map/types.ts"; import { getFrameworkRootFromMeta } from "#veryfront/platform/compat/vfs-paths.ts"; import { Singleflight } from "#veryfront/utils/singleflight.ts"; import { LRUCache } from "#veryfront/utils/lru-wrapper.ts"; @@ -13,6 +14,15 @@ import { fnv1aHash, hashCodeHex } from "#veryfront/utils/hash-utils.ts"; export const LOG_PREFIX = "[SSR-VF-MODULES]"; +// Framework transforms can run after project code has modified shared +// prototypes. Quote each primitive directly with the captured intrinsic so an +// inherited Array.prototype.toJSON cannot collapse otherwise distinct keys. +const JSONStringify = JSON.stringify; + +function quoteCacheIdentityPart(value: string): string { + return JSONStringify(value); +} + // Extensions to try when resolving framework files export const EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"]; @@ -73,11 +83,20 @@ export function buildFrameworkTransformCacheKey( reactVersion: string, projectDir: string, sourceContent: string, + importMapFingerprint?: string, ): string { const contentFingerprint = `${sourceContent.length}:${hashCodeHex(sourceContent)}:${ fnv1aHash(sourceContent) }`; - return JSON.stringify([projectDir, reactVersion, identifier, contentFingerprint]); + const projectPart = quoteCacheIdentityPart(projectDir); + const reactPart = quoteCacheIdentityPart(reactVersion); + const identifierPart = quoteCacheIdentityPart(identifier); + const contentPart = quoteCacheIdentityPart(contentFingerprint); + if (importMapFingerprint === undefined) { + return `[${projectPart},${reactPart},${identifierPart},${contentPart}]`; + } + const importMapPart = quoteCacheIdentityPart(importMapFingerprint); + return `[${projectPart},${reactPart},${importMapPart},${identifierPart},${contentPart}]`; } // Maximum entries for the per-process framework transform caches. @@ -114,6 +133,8 @@ export interface TransformContext { reactVersion: string; projectDir: string; fs: ReturnType; + importMap?: ImportMapConfig; + importMapFingerprint?: string; onProgress?: TransformProgressListener; /** Transform keys already visited by the current recursive traversal. */ transformAncestry?: ReadonlySet; diff --git a/src/transforms/pipeline/stages/ssr-vf-modules/index.ts b/src/transforms/pipeline/stages/ssr-vf-modules/index.ts index 730e9ca032..fd8ccbcc1d 100644 --- a/src/transforms/pipeline/stages/ssr-vf-modules/index.ts +++ b/src/transforms/pipeline/stages/ssr-vf-modules/index.ts @@ -1,4 +1,5 @@ import { CIRCULAR_DEPENDENCY } from "#veryfront/errors"; +import type { ImportMapConfig } from "#veryfront/modules/import-map/types.ts"; /** * SSR VF Modules Stage - resolves /_vf_modules/_veryfront/ paths to framework source. @@ -146,11 +147,16 @@ export const ssrVfModulesPlugin: TransformPlugin = { }); const reactVersion = ctx.reactVersion ?? REACT_DEFAULT_VERSION; + const importMap = ctx.metadata?.get("importMap") as ImportMapConfig | undefined; + const importMapFingerprint = ctx.metadata?.get("importMapFingerprint") as + | string + | undefined; const transformKey = buildFrameworkTransformCacheKey( resolved.sourcePath, reactVersion, ctx.projectDir, resolved.content, + importMapFingerprint, ); const cachePath = await frameworkTransformFlight.do(transformKey, async () => { const transformed = await transformFrameworkSource( @@ -160,6 +166,8 @@ export const ssrVfModulesPlugin: TransformPlugin = { ctx.projectDir, fs, ctx.onProgress, + importMap, + importMapFingerprint, ); // Skip cycle placeholders - don't cache or use them diff --git a/src/transforms/pipeline/stages/ssr-vf-modules/transform.test.ts b/src/transforms/pipeline/stages/ssr-vf-modules/transform.test.ts index 05e95d2f61..1114497147 100644 --- a/src/transforms/pipeline/stages/ssr-vf-modules/transform.test.ts +++ b/src/transforms/pipeline/stages/ssr-vf-modules/transform.test.ts @@ -20,6 +20,7 @@ import { } from "./constants.ts"; import { buildReactUrl } from "#veryfront/transforms/import-rewriter/url-builder.ts"; import { resolveVeryfrontSourcePath } from "./path-resolver.ts"; +import { fnv1aHash, hashCodeHex } from "#veryfront/utils/hash-utils.ts"; describe("reactReExportToEsmUrl", () => { const reactPath = (name: string) => join(FRAMEWORK_ROOT, "react", name); @@ -129,8 +130,24 @@ describe("transformFrameworkCode depth-limit fallback", { const keyA = buildFrameworkTransformCacheKey(sourcePath, "19.2.4", projectA, source); const keyB = buildFrameworkTransformCacheKey(sourcePath, "19.2.4", projectB, source); + const importMapKeyA = buildFrameworkTransformCacheKey( + sourcePath, + "19.2.4", + projectA, + source, + "import-map-v1", + ); + const importMapKeyB = buildFrameworkTransformCacheKey( + sourcePath, + "19.2.4", + projectA, + source, + "import-map-v2", + ); + try { assertEquals(keyA === keyB, false); + assertEquals(importMapKeyA === importMapKeyB, false); await transformFrameworkCode( source, @@ -152,6 +169,66 @@ describe("transformFrameworkCode depth-limit fallback", { } }); + it("preserves framed cache-key bytes under inherited Array.prototype.toJSON", () => { + const identifier = '/framework/quoted"module.ts'; + const reactVersion = "19.2.4"; + const projectDir = "/projects/line\nbreak"; + const source = 'export const marker = "quoted";\n'; + const importMapFingerprint = "import-map-v2"; + const contentFingerprint = `${source.length}:${hashCodeHex(source)}:${fnv1aHash(source)}`; + const expectedLegacy = JSON.stringify([ + projectDir, + reactVersion, + identifier, + contentFingerprint, + ]); + const expectedScoped = JSON.stringify([ + projectDir, + reactVersion, + importMapFingerprint, + identifier, + contentFingerprint, + ]); + const originalToJson = Object.getOwnPropertyDescriptor(Array.prototype, "toJSON"); + let poisonedLegacy: string | undefined; + let poisonedScoped: string | undefined; + let distinctScoped: string | undefined; + + try { + Object.defineProperty(Array.prototype, "toJSON", { + configurable: true, + value: () => [], + }); + poisonedLegacy = buildFrameworkTransformCacheKey( + identifier, + reactVersion, + projectDir, + source, + ); + poisonedScoped = buildFrameworkTransformCacheKey( + identifier, + reactVersion, + projectDir, + source, + importMapFingerprint, + ); + distinctScoped = buildFrameworkTransformCacheKey( + identifier, + reactVersion, + projectDir, + source, + "import-map-v3", + ); + } finally { + if (originalToJson) Object.defineProperty(Array.prototype, "toJSON", originalToJson); + else Reflect.deleteProperty(Array.prototype, "toJSON"); + } + + assertEquals(poisonedLegacy, expectedLegacy); + assertEquals(poisonedScoped, expectedScoped); + assertEquals(poisonedScoped === distinctScoped, false); + }); + it("coalesces concurrent transforms instead of reporting a false cycle", async () => { const tmp = await Deno.makeTempDir({ prefix: "vf-vfmod-concurrent-" }); const sourcePath = `${tmp}/framework-module.ts`; diff --git a/src/transforms/pipeline/stages/ssr-vf-modules/transform.ts b/src/transforms/pipeline/stages/ssr-vf-modules/transform.ts index a93eb38dd4..d6265298c7 100644 --- a/src/transforms/pipeline/stages/ssr-vf-modules/transform.ts +++ b/src/transforms/pipeline/stages/ssr-vf-modules/transform.ts @@ -217,6 +217,7 @@ async function transformAndCacheFallbackDep( ctx.reactVersion, ctx.projectDir, depContent, + ctx.importMapFingerprint, ); // Prefer the main path's fully-resolved cache entry when present — // that output is strictly higher quality than what the fallback @@ -337,7 +338,7 @@ async function rewriteFallbackRelativeImports( // from file://, and Node rejects `import ... from "https:"` // (ERR_UNSUPPORTED_ESM_URL_SCHEME); leaving the remote specifier in would // break SSR under Node whenever a deep framework file hits this fallback. - const importMap = await loadImportMap(ctx.projectDir); + const importMap = ctx.importMap ?? (await loadImportMap(ctx.projectDir)); const cacheResult = await cacheHttpImportsToLocal(rewritten, { cacheDir: getHttpBundleCacheDir(), importMap, @@ -364,6 +365,7 @@ export async function transformFrameworkCode( ctx.reactVersion, ctx.projectDir, content, + ctx.importMapFingerprint, ); const ancestry = ctx.transformAncestry ?? new Set(); @@ -428,6 +430,7 @@ async function transformFrameworkCodeUncoalesced( ctx.reactVersion, ctx.projectDir, content, + ctx.importMapFingerprint, ); const cached = frameworkFileCache.get(transformKey); if (cached) { @@ -554,6 +557,7 @@ async function transformFrameworkCodeUncoalesced( ctx.reactVersion, ctx.projectDir, depContent, + ctx.importMapFingerprint, ); const existingFileUrl = frameworkFileCache.get(dependencyTransformKey); if (existingFileUrl) { @@ -640,7 +644,7 @@ async function transformFrameworkCodeUncoalesced( transformed = await stripJsonAttributesFromModuleImports(transformed); // Cache HTTP imports to local filesystem - const importMap = await loadImportMap(ctx.projectDir); + const importMap = ctx.importMap ?? (await loadImportMap(ctx.projectDir)); const cacheResult = await cacheHttpImportsToLocal(transformed, { cacheDir: getHttpBundleCacheDir(), importMap, @@ -676,6 +680,7 @@ export async function resolveAndTransformVeryfrontImport( ctx.reactVersion, ctx.projectDir, content, + ctx.importMapFingerprint, ); const cached = veryfrontTransformCache.get(transformKey); if (cached) { @@ -755,11 +760,13 @@ export async function transformFrameworkSource( projectDir: string, fs: ReturnType, onProgress?: TransformContext["onProgress"], + importMap?: TransformContext["importMap"], + importMapFingerprint?: string, ): Promise { return transformFrameworkCode( content, sourcePath, - { reactVersion, projectDir, fs, onProgress }, + { reactVersion, projectDir, fs, onProgress, importMap, importMapFingerprint }, true, ); } diff --git a/src/transforms/pipeline/types.ts b/src/transforms/pipeline/types.ts index 2008887e71..62887b2b7f 100644 --- a/src/transforms/pipeline/types.ts +++ b/src/transforms/pipeline/types.ts @@ -6,6 +6,9 @@ */ import type { DependencyHashCache } from "#veryfront/cache/dependency-graph.ts"; +import type { PreloadImportMapContext } from "#veryfront/modules/import-map/preloader.ts"; +import type { ImportMapConfig } from "#veryfront/modules/import-map/types.ts"; +import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts"; import type { TransformProgressListener } from "#veryfront/transforms/progress.ts"; import type { DependencyPinningSourceInput } from "../esm/package-registry.ts"; import type { DependencyResolutionObservation } from "../import-rewriter/dependency-resolution.ts"; @@ -62,6 +65,12 @@ export interface TransformOptions { studioEmbed?: boolean; /** React version to use (detected from project package.json if not provided) */ reactVersion?: string; + /** Immutable import-map snapshot already selected for this render. */ + preloadedImportMap?: ImportMapConfig; + /** Adapter used to load and cache the project import map before SSR cache identity. */ + importMapAdapter?: RuntimeAdapter; + /** Content-source/config identity for the import-map preloader. */ + importMapPreloadContext?: PreloadImportMapContext; /** File reader for dependency hash computation. When provided, enables dependency-aware cache invalidation. */ readFile?: (path: string) => Promise; /** Internal per-render dependency hash cache. */ @@ -141,8 +150,17 @@ export interface TransformContext { export interface TransformPlugin { /** Plugin name for logging/debugging */ name: string; - /** Stage this plugin runs at */ + /** + * Numeric ordering coordinate for this plugin. + * TransformStage values are phase anchors; finite fractional values may run + * between anchors when a plugin needs a stable intermediate position. + */ stage: TransformStage; + /** + * Stable, versioned identity for output-affecting custom plugin behavior. + * Custom plugins without an identity still run, but disable persistent caching. + */ + cacheIdentity?: string; /** Optional condition - if false, plugin is skipped */ condition?: (ctx: TransformContext) => boolean; /** Transform function - returns new code */ diff --git a/src/transforms/shared/server-only-packages.ts b/src/transforms/shared/server-only-packages.ts index ffe25d504f..e14ef57c35 100644 --- a/src/transforms/shared/server-only-packages.ts +++ b/src/transforms/shared/server-only-packages.ts @@ -32,6 +32,22 @@ const SERVER_ONLY_PACKAGES: ReadonlySet = new Set([ "oracledb", "cassandra-driver", ]); +const ReflectApply = Reflect.apply; +const SetHas = Set.prototype.has; +const StringSlice = String.prototype.slice; +const StringStartsWith = String.prototype.startsWith; + +function setHas(set: ReadonlySet, value: T): boolean { + return ReflectApply(SetHas, set, [value]) as boolean; +} + +function stringSlice(value: string, start: number): string { + return ReflectApply(StringSlice, value, [start]) as string; +} + +function stringStartsWith(value: string, search: string): boolean { + return ReflectApply(StringStartsWith, value, [search]) as boolean; +} /** * True if a bare package specifier's package name is a known server-only @@ -42,8 +58,10 @@ const SERVER_ONLY_PACKAGES: ReadonlySet = new Set([ * before matching so both `redis` and `npm:redis@5.11.0` are recognized. */ export function isServerOnlyPackage(packageName: string): boolean { - const bare = packageName.startsWith("npm:") ? packageName.slice("npm:".length) : packageName; - return SERVER_ONLY_PACKAGES.has(bare); + const bare = stringStartsWith(packageName, "npm:") + ? stringSlice(packageName, "npm:".length) + : packageName; + return setHas(SERVER_ONLY_PACKAGES, bare); } export { SERVER_ONLY_PACKAGES }; diff --git a/src/utils/hash-utils.test.ts b/src/utils/hash-utils.test.ts index 4f777d1f4d..8afa754bf6 100644 --- a/src/utils/hash-utils.test.ts +++ b/src/utils/hash-utils.test.ts @@ -33,6 +33,47 @@ describe("hash-utils", () => { const hash = await computeHash("こんにちは世界"); assertEquals(hash.length, 64); }); + + it("uses captured typed-array accessors after prototype poisoning", async () => { + const lengthDescriptor = Object.getOwnPropertyDescriptor( + Uint8Array.prototype, + "length", + ); + const byteLengthDescriptor = Object.getOwnPropertyDescriptor( + Uint8Array.prototype, + "byteLength", + ); + let hash: string | undefined; + try { + Object.defineProperty(Uint8Array.prototype, "length", { + configurable: true, + get: () => 0, + }); + Object.defineProperty(Uint8Array.prototype, "byteLength", { + configurable: true, + get: () => 0, + }); + hash = await computeHash("typed-array-accessor-regression"); + } finally { + if (lengthDescriptor) { + Object.defineProperty(Uint8Array.prototype, "length", lengthDescriptor); + } else { + Reflect.deleteProperty(Uint8Array.prototype, "length"); + } + if (byteLengthDescriptor) { + Object.defineProperty( + Uint8Array.prototype, + "byteLength", + byteLengthDescriptor, + ); + } else { + Reflect.deleteProperty(Uint8Array.prototype, "byteLength"); + } + } + + assertEquals(hash?.length, 64); + assertEquals(hash, await computeHash("typed-array-accessor-regression")); + }); }); describe("computeCodeHash", () => { diff --git a/src/utils/hash-utils.ts b/src/utils/hash-utils.ts index 06036e54a7..c96caf7bf7 100644 --- a/src/utils/hash-utils.ts +++ b/src/utils/hash-utils.ts @@ -4,19 +4,66 @@ import { HASH_SEED_FNV1A } from "./constants/hash.ts"; /** Number of hex characters kept by shortHash (8 hex chars = 32 bits) */ const SHORT_HASH_LENGTH = 8; +// Hashes participate in cache and request identities after project modules may +// have executed in the shared realm. Capture the small set of primordials used +// by that boundary before project code can replace their implementations. +const IntrinsicTextEncoder = TextEncoder; +const IntrinsicUint8Array = Uint8Array; +const NumberPrototypeToString = Number.prototype.toString; +const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const ObjectGetPrototypeOf = Object.getPrototypeOf; +const ReflectApply = Reflect.apply; +const StringPrototypePadStart = String.prototype.padStart; +const SubtleCryptoDigest = crypto.subtle.digest; +const TextEncoderPrototypeEncode = TextEncoder.prototype.encode; +const cryptoSubtle = crypto.subtle; +const hashTextEncoder = new IntrinsicTextEncoder(); +const TypedArrayPrototype = ObjectGetPrototypeOf(IntrinsicUint8Array.prototype); +const TypedArrayLengthGetter = ObjectGetOwnPropertyDescriptor( + TypedArrayPrototype, + "length", +)!.get!; + +function typedArrayLength(value: Uint8Array): number { + return ReflectApply(TypedArrayLengthGetter, value, []) as number; +} + function toHex(buffer: ArrayBuffer): string { - return Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, "0")).join(""); + const bytes = new IntrinsicUint8Array(buffer); + let result = ""; + const length = typedArrayLength(bytes); + for (let index = 0; index < length; index++) { + const hex = ReflectApply(NumberPrototypeToString, bytes[index], [16]) as string; + result += ReflectApply(StringPrototypePadStart, hex, [2, "0"]) as string; + } + return result; } /** Compute the lowercase hex SHA-256 digest of a UTF-8 string. */ export async function computeHash(content: string): Promise { - const data = new TextEncoder().encode(content); - return toHex(await crypto.subtle.digest("SHA-256", data)); + const data = ReflectApply( + TextEncoderPrototypeEncode, + hashTextEncoder, + [content], + ) as Uint8Array; + return toHex( + await ReflectApply( + SubtleCryptoDigest, + cryptoSubtle, + ["SHA-256", data], + ) as ArrayBuffer, + ); } /** Compute the lowercase hex SHA-256 digest of raw bytes. */ export async function computeHashBytes(bytes: BufferSource): Promise { - return toHex(await crypto.subtle.digest("SHA-256", bytes)); + return toHex( + await ReflectApply( + SubtleCryptoDigest, + cryptoSubtle, + ["SHA-256", bytes], + ) as ArrayBuffer, + ); } /** Source bundle content used for hash computation. */ export interface BundleCode { diff --git a/tests/integration/module-loading/import-map-loader.test.ts b/tests/integration/module-loading/import-map-loader.test.ts index 7ddbfbc027..3ea79e7a35 100644 --- a/tests/integration/module-loading/import-map-loader.test.ts +++ b/tests/integration/module-loading/import-map-loader.test.ts @@ -43,7 +43,7 @@ describe("import-map-loader", () => { }); }); - it("should load deno.json with both imports and scopes", async () => { + it("should load deno.json imports and strip scoped framework overrides", async () => { await withImportMapTestContext("import-map-load-scopes", async (context, adapter) => { const denoConfig = { imports: { @@ -73,7 +73,7 @@ describe("import-map-loader", () => { assertExists(importMap.scopes); assertEquals(typeof importMap.scopes, "object"); - assertEquals(importMap.scopes?.["/vendor/"]?.["react"], "https://esm.sh/react@17.0.2"); + assertEquals(importMap.scopes?.["/vendor/"]?.["react"], undefined); }); });