web/repo: a rendered diff page ships the hunks it draws instead of both whole sides, and reads the sides only to expand
Signed-off-by: dawn <90008@klbr.net>
6 unmodified lines789101111121314151617410 unmodified linesExpand all4284294304314304314324334344354364374384394404414424434324334344354364374384394404414424434444454454464474484494504514474484494504514524524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085096 unmodified lines isDiffTooLarge, fetchSides, loadDiff, loadDiffStat, withSides patchesScript, readPatches, shippedPatches} from "#lib/api/repoDiff.ts";import { emptyTreeOid, type MirrorFileDiff } from "#lib/api/knotmirror.ts";import type { Fetch } from "#lib/fetch.ts";
410 unmodified linesExpand all expect(res.status).toBe(500); });});
describe("withSides", () => { it("ships the text of the first files that fit, and leaves the rest to the browser", async () => { const file = (path: string, size: number) => ({ lhsSrc: src(path, { size }), rhsSrc: src(path, { size }), hunks: [{ novelLhs: [0], novelRhs: [0], lines: [{ lhs: 0, rhs: 0 }] }], hasSyntacticChanges: false }); const { deps, calls } = harness([file("a.ts", 4), file("b.ts", 40), file("c.ts", 1)], { "base:a.ts": "one\n", "head:a.ts": "two\n", "base:c.ts": "x", "head:c.ts": "y" });describe("readPatches", () => { const file = (path: string, size: number) => ({ lhsSrc: src(path, { size }), rhsSrc: src(path, { size }), hunks: [{ novelLhs: [0], novelRhs: [0], lines: [{ lhs: 0, rhs: 0 }] }], hasSyntacticChanges: false }); const texts = { "base:a.ts": "one\n", "head:a.ts": "two\n", "base:c.ts": "x", "head:c.ts": "y" };
const diff = await withSides(deps, await loadDiff(deps, diffSpec), 50); it("ships the hunks of the first files it reads, and leaves the rest to the browser", async () => { const { deps, calls } = harness( [file("a.ts", 4), file("b.ts", 40), file("c.ts", 1)], texts );
expect( diff.files.map((file) => [file.sides?.lhs.content, file.sides?.rhs.content]) ).toEqual([ ["one\n", "two\n"], [undefined, undefined], [undefined, undefined] const diff = await readPatches(deps, await loadDiff(deps, diffSpec), 64, 50);
expect(diff.files.map((file) => file.patch)).toEqual([ { text: "@@ -1,1 +1,1 @@\n-one\n+two\n", trailing: 0, type: "change" }, undefined, undefined ]); expect(blobCalls(calls)).toHaveLength(2); });
it("counts the patch it ships against the budget, not the sides", async () => { const { deps } = harness([file("a.ts", 4), file("c.ts", 1)], texts);
const diff = await readPatches(deps, await loadDiff(deps, diffSpec), 30, 1024);
expect(diff.files.map((file) => file.patch?.text.length)).toEqual([26, undefined]); });
it("reads the sides from a clone, which kit doesn't inline into the page", async () => { const { deps } = harness([file("a.ts", 4)], texts); const read: string[] = []; const watched: Fetch = async (input, init) => new Proxy(await deps.git.ctx.fetch(input, init), { get: (response, key) => { read.push(String(key)); const value: unknown = Reflect.get(response, key, response); return typeof value === "function" ? value.bind(response) : value; } }); const git = { ...deps.git, ctx: { ...deps.git.ctx, fetch: watched } };
const diff = await readPatches({ ...deps, git }, await loadDiff(deps, diffSpec));
expect(diff.files[0]?.patch).toBeDefined(); expect(read).toContain("clone"); expect(read.filter((key) => ["body", "text", "json", "arrayBuffer"].includes(key))).toEqual( [] ); });
it("hands the patches it shipped to the app's first load, in a script nothing closes early", async () => { const { deps } = harness([file("a.ts", 4), file("c.ts", 1)], { ...texts, "head:a.ts": "</script>\n" }); const loaded = await loadDiff(deps, diffSpec); const shipped = await readPatches(deps, loaded); const script = patchesScript(shipped) ?? ""; const body = script.slice(script.indexOf(">") + 1, script.lastIndexOf("</script>")); const root = { querySelectorAll: () => [{ textContent: body }] } as unknown as ParentNode;
expect(body).not.toContain("<"); expect(shippedPatches(loaded, root).files.map((file) => file.patch)).toEqual( shipped.files.map((file) => file.patch) ); expect(shipped.files.map((file) => file.patch === undefined)).toEqual([false, false]); });});123453 unmodified lines9101112131415161730 unmodified lines48495051525354555657146 unmodified linesExpand all20420520620720420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926020926126226321226426526621421521621721821922022122222322422522622722822923023126726826927027127227327427523327627727827928028128228328428528628728820 unmodified linesimport { browser } from "$app/env";import { MAX_INLINE_SIZE } from "#lib/api/blob.ts";import { blobText, knotMirrorTarget, type GitServiceConfig } from "#lib/api/gitclient.ts";import { diffFileKind,3 unmodified lines type MirrorFileDiff, type MirrorFileKind} from "#lib/api/knotmirror.ts";import type { FileContents } from "@pierre/diffs";import { hunkPatch, type HunkPatch } from "#lib/api/diffPatch.ts";import type * as ShTangledGitDefs from "#lib/api/lexicons/types/sh/tangled/git/defs.ts";import type { Did } from "@atcute/lexicons/syntax";import type { BobbinContext } from "#lib/api/client.ts";import type { GitTarget } from "#lib/api/gitclient.ts";30 unmodified lines lines: number; // Explains unrenderable files (binary, submodule, oversized); suppresses sides fetch. note?: string; sides?: { lhs: DiffSrc; rhs: DiffSrc }; // the hunks a server render shipped, so the browser draws them without the sides patch?: HunkPatch;}
export interface ContentsSource { oldRef: string;146 unmodified linesExpand all return null; });};
// a server render ships the text of the first files with the page, so the browser neither// fetches their sides nor waits for them to paint; past the cap it fetches the rest as it// scrolls, as beforeexport const SSR_SIDES_BYTES = 64 * 1024;// a server render reads the sides of the first files and ships the hunks pierre draws from// them, so the browser neither reads them nor waits for them to paint; past either cap it// reads the rest as it scrolls, as beforeexport const SSR_PATCH_BYTES = 64 * 1024;export const SSR_SIDES_READ_BYTES = 512 * 1024;
const PATCHES_ATTRIBUTE = "data-diff-patches";
// a patch is named by the sides it was built from, so one never lands on another diff's fileconst sidesKey = (sides: { lhs: DiffSrc; rhs: DiffSrc }): string => `${sides.lhs.oid}:${sides.rhs.oid}`;
/** the script a page carries its diff's patches in, for the app to take up as it starts */export const patchesScript = (diff: RepoDiff): string | null => { const patches = Object.fromEntries( diff.files.flatMap((file) => file.patch && file.sides ? [[sidesKey(file.sides), file.patch]] : [] ) ); if (Object.keys(patches).length === 0) return null; const json = JSON.stringify(patches).replaceAll("<", "\\u003c"); return `<script type="application/json" ${PATCHES_ATTRIBUTE}>${json}</script>`;};
/** the patches the page came with, which the app's first load draws without reading */export const shippedPatches = (diff: RepoDiff, root: ParentNode = document): RepoDiff => { const patches: Record<string, HunkPatch> = Object.assign( {}, ...[...root.querySelectorAll(`script[${PATCHES_ATTRIBUTE}]`)].map((script) => JSON.parse(script.textContent ?? "{}") ) ); return { ...diff, files: diff.files.map((file) => { const patch = file.sides && patches[sidesKey(file.sides)]; return patch ? { ...file, patch } : file; }) };};
// kit inlines into the page every response its fetch reads, but not a clone's, so the sides// a patch is built from stay out of itconst unshipped = (fetch: Fetch): Fetch => async (input, init) => (await fetch(input, init)).clone();
/** a first load's diff with the patches it ships: read on the server, taken from the page in the browser */export const withPatches = (deps: RepoDiffDeps, diff: RepoDiff): Promise<RepoDiff> => browser ? Promise.resolve(shippedPatches(diff)) : readPatches(deps, diff);
export const withSides = async (export const readPatches = async ( deps: RepoDiffDeps, diff: RepoDiff, maxBytes: number = SSR_SIDES_BYTES maxBytes: number = SSR_PATCH_BYTES, maxReadBytes: number = SSR_SIDES_READ_BYTES): Promise<RepoDiff> => { let left = maxBytes; const shipped = diff.files.map((file) => { if (left <= 0 || !file.sides) return Promise.resolve(file); left -= file.sides.lhs.size + file.sides.rhs.size; if (left < 0) return Promise.resolve(file); return fetchSides(deps, diff.contents, file).then( (pair): RepoDiffFile => "oldFile" in pair && file.sides ? { ...file, sides: { lhs: { ...file.sides.lhs, content: pair.oldFile.contents }, rhs: { ...file.sides.rhs, content: pair.newFile.contents } } } : file, () => file ); const ctx = { ...deps.git.ctx, fetch: unshipped(deps.git.ctx.fetch) }; const reading = { ...deps, git: { ...deps.git, ctx } }; let unread = maxReadBytes; const reads = diff.files.map((file) => { if (unread <= 0 || !file.sides) return Promise.resolve(null); unread -= file.sides.lhs.size + file.sides.rhs.size; if (unread < 0) return Promise.resolve(null); return fetchSides(reading, diff.contents, file).catch(() => null); }); return { ...diff, files: await Promise.all(shipped) }; let left = maxBytes; const files: RepoDiffFile[] = []; for (const [index, file] of diff.files.entries()) { const pair = left > 0 ? await reads[index] : null; const patch = pair && "oldFile" in pair ? hunkPatch(pair.oldFile, pair.newFile) : null; if (patch) left -= patch.text.length; files.push(patch && left >= 0 ? { ...file, patch } : file); } return { ...diff, files };};
export const fetchSides = async ( deps: RepoDiffDeps,20 unmodified lines1223456719 unmodified lines27282930303132313233343536373837 unmodified linesimport { getContext, setContext } from "svelte";import type { FileContents, FileDiffOptions, FileOptions } from "@pierre/diffs";import type { FileDiffOptions, FileOptions } from "@pierre/diffs";import type { HunkPatch } from "#lib/api/diffPatch.ts";import type { MarkupContext } from "#lib/markup/paths.ts";import { prerender } from "#lib/prerender.ts";
// past any of these limits the browser renders it instead19 unmodified linesexport type RenderTask = | { kind: "markup"; source: string; ctx: MarkupContext } | { kind: "file"; name: string; contents: string; options: FileOptions<undefined, undefined> } | { kind: "diff"; oldFile: FileContents; newFile: FileContents; kind: "patch"; patch: HunkPatch; name: string; oldName?: string; options: FileDiffOptions<undefined, undefined>; };
export interface SsrBudget {37 unmodified lines8 unmodified lines91011121314131415161718192021222324252627282924 unmodified lines8 unmodified lines
export const runTask = async (task: RenderTask): Promise<string> => { if (task.kind === "markup") return renderMarkup(task.source, task.ctx); await import("#lib/syntax.ts"); const { preloadFile, preloadMultiFileDiff } = await import("@pierre/diffs/ssr"); if (task.kind === "diff") return (await preloadMultiFileDiff(task)).prerenderedHTML; const { preloadFile, preloadFileDiff } = await import("@pierre/diffs/ssr"); if (task.kind === "patch") { const { patchDiff } = await import("#lib/api/diffPatch.ts"); const fileDiff = patchDiff(task.patch, task, ""); // draws the separators of a diff the browser can expand, as its viewer will. a function // can't cross into the worker, and the server never expands anything const loadDiffFiles = () => Promise.reject(new Error("the server expands nothing")); const { prerenderedHTML } = await preloadFileDiff({ fileDiff, options: { ...task.options, loadDiffFiles } }); return prerenderedHTML; } const { prerenderedHTML } = await preloadFile({ file: { name: task.name, contents: task.contents }, options: task.options });24 unmodified lines2 unmodified lines34567891011121314141516171819202122232425262733 unmodified lines6162636463646566676566676869704 unmodified lines7576777879808182838413 unmodified lines989910010110210310410510616 unmodified lines12312412512612512712812912812913013113113213313413543 unmodified lines179180181182183184185186187521 unmodified linesExpand all70971071171271271371371471571571671771871971671771871972072114 unmodified lines2 unmodified lines import { CodeView, type CodeViewDiffItem, type CodeViewLineSelection, type FileDiffContentsLoader, type FileDiffMetadata, type SelectedLineRange, type SelectionSide, type VirtualizedFileDiff } from "@pierre/diffs"; import { diffLineSpan } from "#lib/components/repo/lineAnchor.ts"; import { prerenderDiff, prerenderPatchDiff, retainPierreWorkerPool, pierreCodeViewOptions, PIERRE_ITEM_METRICS, PIERRE_VALIDATE_ITEM_HEIGHTS, PIERRE_LINE_HEIGHT, type DiffStyle } from "#lib/components/repo/pierre.ts"; import type { HunkPatch } from "#lib/api/diffPatch.ts"; import { remToPx } from "#lib/rem.ts"; import { getSsrBudget } from "#lib/ssr/render.ts"; import { scrollHostOf, type ScrollHost } from "#lib/components/repo/scrollHost.ts"; import DiffFileHeader from "#lib/components/repo/DiffFileHeader.svelte";33 unmodified lines kind: "new" | "deleted" | "renamed" | "change"; stat: { insertions: number; deletions: number }; note?: string; lines?: number; // set on the files whose text came with the page sides?: { lhs: { path: string; content?: string }; rhs: { path: string; content?: string }; }; // set on the files whose hunks came with the page patch?: HunkPatch; }
interface Props { files: CodeViewFile[];4 unmodified lines wrap?: boolean; isOpen?: (key: string) => boolean; onSetOpen?: (key: string, open: boolean) => void; onWant?: (key: string) => void; // the whole sides of a file drawn from its patch, to expand its context loadSides?: FileDiffContentsLoader; onFocus?: (key: string) => void; dockTop?: () => number; onScrollExtent?: (max: number) => void; onLineSelected?: (key: string, range: SelectedLineRange | null) => void;13 unmodified lines wrap = false, isOpen, onSetOpen, onWant, loadSides, onFocus, dockTop = () => 0, onScrollExtent, onLineSelected,16 unmodified lines let painted = $state(0); let view: CodeView | undefined = $state.raw(); let initError = $state<string | undefined>();
// the files whose text came with the page, up to the first that didn't, drawn by the server // the files whose hunks came with the page, up to the first that didn't, drawn by the server // and standing until the viewer has painted them const budget = getSsrBudget(); const shippedText = (file: CodeViewFile) => file.sides?.lhs.content !== undefined && file.sides.rhs.content !== undefined; const shipped = $derived.by(() => { const end = files.findIndex((file) => !file.note && !shippedText(file)); const end = files.findIndex((file) => !file.note && !file.patch); return end === -1 ? files : files.slice(0, end); }); let held = $state(true); let filledView = $state(false);43 unmodified lines };
const codeViewOptions = (style: DiffStyle, forWrap: boolean) => ({ ...pierreCodeViewOptions(style, forWrap), loadDiffFiles: loadSides, disableFileHeader: false, // Presence of renderCustomHeader activates custom-header slot in Pierre's renderer. renderCustomHeader: () => undefined, itemMetrics: {521 unmodified linesExpand all {showStat} onToggle={() => onSetOpen?.(file.key, !open)} /> <diffs-container class="diffs-container"> {#if budget && open && file.sides && shippedText(file)} {@const html = await prerenderDiff( {#if budget && open && file.patch} {@const html = await prerenderPatchDiff( budget, { name: file.sides.lhs.path || file.name, contents: file.sides.lhs.content ?? "" }, { name: file.name, contents: file.sides.rhs.content ?? "" }, file.patch, file, diffStyle, wrap )} {#if html}14 unmodified lines8 unmodified lines9101112131314151617108 unmodified linesExpand all1261271281291301311321331341 unmodified line1361371381391401411421431441451461474 unmodified lines1521531541551561571581591605 unmodified lines8 unmodified lines type CodeViewFile, type DiffStock } from "#lib/components/repo/DiffCodeView.svelte"; import { fileAnchor, jumpToAnchoredFile } from "#lib/components/repo/fileAnchor.svelte.ts"; import type { RepoDiff, RepoDiffDeps } from "#lib/api/repoDiff.ts"; import { patchesScript, type RepoDiff, type RepoDiffDeps } from "#lib/api/repoDiff.ts"; import type { DiffStyle } from "#lib/components/repo/pierre.ts"; import type { DiffEntry } from "#lib/components/repo/fileDiff.ts"; import { createDiffQueue } from "#lib/components/repo/diffQueue.ts"; import { diffRows, findDiffMatches, type DiffMatch } from "#lib/components/lines/diff.ts";108 unmodified linesExpand all return found; };
export const resync = () => view?.resync(); const shippedPatches = $derived(patchesScript(diff)); jumpToAnchoredFile(jumpToFile);</script>
<!-- one scroller for the whole diff: the page itself never grows with the file1 unmodified line a diff of this size --><!-- fills whatever the sticky toolbar leaves of the column, so the scroller always reaches the bottom of the card --><div class="flex min-h-0 shrink grow basis-auto flex-col"> {#if shippedPatches} <!-- eslint-disable-next-line svelte/no-at-html-tags -- json of our own patches, its < escaped --> {@html shippedPatches} {/if} <DiffCodeView bind:this={view} files={viewFiles} entry={(key) => entries.get(key)}4 unmodified lines onSetOpen={(key, open) => onSetOpen?.(key, open)} {dockTop} {onScrollExtent} onWant={(key) => queue.want(key)} loadSides={queue.loadSides} onFocus={(key) => queue.focus(key)} onLineSelected={linkLines ? selectLines : undefined} onQuoteLines={onQuoteLines && quoteLines} {matches}5 unmodified lines2 unmodified lines34567891011127 unmodified lines202122232425262728294 unmodified lines34353637383940414248 unmodified lines919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126991271281021031041051061071081091291301311321331345 unmodified lines14014114214314414514614714814915015115215315415515615715810 unmodified lines2 unmodified lines type RepoDiff, type RepoDiffDeps, type RepoDiffFile} from "#lib/api/repoDiff.ts";import type { FileDiffLoadedChangedFiles, FileDiffMetadata } from "@pierre/diffs";import { patchDiff } from "#lib/api/diffPatch.ts";import { parseDiffInWorker } from "#lib/components/repo/diffRpc.ts";import type { DiffEntry } from "#lib/components/repo/fileDiff.ts";
export type { DiffEntry };7 unmodified lines
export interface DiffQueue { focus: (key: string) => void; want: (key: string) => void; // the whole sides of a file drawn from its patch, for pierre to expand it loadSides: (diff: FileDiffMetadata) => Promise<FileDiffLoadedChangedFiles>; destroy: () => void;}
const CONCURRENCY = 6;4 unmodified lines const inFlight = new Set<string>(); // file order is monotonic down the page, so index distance stands in for // pixels let focusIndex = 0; const patched = new WeakMap<FileDiffMetadata, RepoDiffFile>();
let indexed: { files: RepoDiffFile[]; byKey: Map<string, { file: RepoDiffFile; index: number }>;48 unmodified lines } } };
// pierre caches highlights by these keys, and a path alone repeats across a pull's versions const cacheKeys = (file: RepoDiffFile) => ({ old: `${file.key}:old:${file.sides?.lhs.oid ?? ""}`, new: `${file.key}:new:${file.sides?.rhs.oid ?? ""}` });
const fromPatch = (file: RepoDiffFile): FileDiffMetadata | undefined => { if (!file.patch) return undefined; try { const keys = cacheKeys(file); const diff = patchDiff(file.patch, file, `${keys.old}:${keys.new}:patch`); patched.set(diff, file); return diff; } catch { return undefined; } };
const fill = async (file: RepoDiffFile) => { running++; inFlight.add(file.key); try { if (host.entry(file.key)) return; const drawn = fromPatch(file); if (drawn) { host.put({ [file.key]: drawn }); return; } const contents = await fetchWithRetry(file); if (!("oldFile" in contents)) { host.put({ [file.key]: { note: contents.note } }); } else { // pierre caches highlights by these keys, and a path alone repeats across a pull's versions host.put({ [file.key]: await parseDiffInWorker( { ...contents.oldFile, cacheKey: `${file.key}:old:${file.sides?.lhs.oid ?? ""}` }, { ...contents.newFile, cacheKey: `${file.key}:new:${file.sides?.rhs.oid ?? ""}` } { ...contents.oldFile, cacheKey: cacheKeys(file).old }, { ...contents.newFile, cacheKey: cacheKeys(file).new } ) }); } } catch {5 unmodified lines } };
return { loadSides: async (diff) => { const file = patched.get(diff); if (!file) throw new Error("no file drawn from this diff's patch"); const contents = await fetchWithRetry(file); if (!("oldFile" in contents)) throw new Error(contents.note); const keys = cacheKeys(file); return { oldFile: { ...contents.oldFile, cacheKey: keys.old }, newFile: { ...contents.newFile, cacheKey: keys.new } }; }, destroy: () => { pending.clear(); }, focus: (key) => {10 unmodified lines12334567882 unmodified lines919293949495969596979898999910010110210310310410510610410510610710810911011111285 unmodified lines// the css side of pierre theming lives in src/pierre.css
import type { FileContents, FileDiffOptions, FileOptions, VirtualFileMetrics } from "@pierre/diffs";import type { FileDiffOptions, FileOptions, VirtualFileMetrics } from "@pierre/diffs";import type { HunkPatch } from "#lib/api/diffPatch.ts";import { browser } from "$app/env";import { textCheck } from "#lib/api/blob.ts";import diffShadowCSS from "#lib/components/repo/pierreDiff.css?raw";import { remToPx } from "#lib/rem.ts";82 unmodified lines contents, options: pierreFileOptions(false) }));
// one file of a diff as the diff viewer draws it, less the header the page renders itself.// null means the browser renders itexport const prerenderDiff = (// one file of a diff as the diff viewer draws it from its hunks, less the header the page// renders itself. null means the browser renders itexport const prerenderPatchDiff = ( budget: SsrBudget, oldFile: FileContents, newFile: FileContents, patch: HunkPatch, names: { name: string; oldName?: string }, style: DiffStyle, wrap: boolean): Promise<string | null> => renderOnServer(budget, oldFile.contents.length + newFile.contents.length, () => ({ kind: "diff", oldFile, newFile, renderOnServer(budget, patch.text.length, () => ({ kind: "patch", patch, name: names.name, oldName: names.oldName, options: { ...pierreCodeViewOptions(style, wrap), disableFileHeader: true } }));
// lazy dynamic imports: pierre's worker bundle trips vite's dev dependency optimizer85 unmodified lines12 unmodified lines1314151617171819202142 unmodified lines6465666768686970717269 unmodified lines12 unmodified linesimport { cappedFetch, isDiffTooLarge, loadDiff, withSides, withPatches, type RepoDiffDeps, type RepoDiffSpec} from "#lib/api/repoDiff.ts";
42 unmodified lines let diffSpec: RepoDiffSpec | undefined; let diffError: string | undefined; try { diff = await loadDiff(diffDeps, spec); if (renderedPage) diff = await withSides(diffDeps, diff); if (renderedPage) diff = await withPatches(diffDeps, diff); } catch (cause) { if (isDiffTooLarge(cause)) diffSpec = spec; else diffError = errorMessage(cause, "could not load commit diff from the mirror"); }69 unmodified lines2 unmodified lines34567789101150 unmodified lines6263646566666768697015 unmodified lines2 unmodified linesimport { gitTarget } from "#lib/api/gitclient.ts";import { listLog } from "#lib/api/knotmirror.ts";import { early, orNull, settle, stream } from "#lib/api/load.ts";import { isRedirect, parseDiffRoute } from "#lib/api/pullRoute.ts";import { cappedFetch, loadDiff, withSides } from "#lib/api/repoDiff.ts";import { cappedFetch, loadDiff, withPatches } from "#lib/api/repoDiff.ts";import { withAuthorHandles } from "#lib/api/repoIndex.ts";import { countCommentsByCommit } from "#lib/components/comment/comments.ts";import { didFromUri } from "#lib/api/uri.ts";import { servesRenderedPage } from "#lib/ssr/firstLoad.ts";50 unmodified lines const diff = version && commits !== null ? await orNull( loadDiff(diffDeps, { repo: repo.repoDid as Did, version }).then((loaded) => renderedPage ? withSides(diffDeps, loaded) : loaded renderedPage ? withPatches(diffDeps, loaded) : loaded ) ) : null;
15 unmodified lines