rerender-lens 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -1
- package/README.md +14 -5
- package/dist/{devtools-DmhWiWcN.d.cts → devtools-BkCct3cJ.d.cts} +10 -2
- package/dist/{devtools-BHGUUo-p.d.ts → devtools-CZebzpF6.d.ts} +10 -2
- package/dist/index.cjs +199 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +199 -71
- package/dist/index.js.map +1 -1
- package/dist/playwright.cjs +5 -2
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.cts +5 -3
- package/dist/playwright.d.ts +5 -3
- package/dist/playwright.js +5 -3
- package/dist/playwright.js.map +1 -1
- package/dist/rerender-lens.iife.js +768 -307
- package/dist/setup.cjs +199 -71
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +199 -71
- package/dist/setup.js.map +1 -1
- package/dist/vitest-setup.cjs +117 -48
- package/dist/vitest-setup.cjs.map +1 -1
- package/dist/vitest-setup.js +117 -48
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +117 -48
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.js +117 -48
- package/dist/vitest.js.map +1 -1
- package/package.json +2 -2
- package/panel/panel.css +0 -34
- package/panel/panel.js +400 -438
package/dist/playwright.cjs
CHANGED
|
@@ -140,12 +140,14 @@ ${formatFixes(reports)}`);
|
|
|
140
140
|
|
|
141
141
|
// src/playwright.ts
|
|
142
142
|
var bundleCache = null;
|
|
143
|
+
var VENDOR_FILES = ["rerender-lens.core.js", "rerender-lens.engine.js", "rerender-lens.js"];
|
|
143
144
|
function libraryBundle() {
|
|
144
145
|
if (bundleCache) return bundleCache;
|
|
145
146
|
const here = path.dirname(url.fileURLToPath(importMetaUrl));
|
|
146
|
-
|
|
147
|
+
const candidates = [[path.join(here, "rerender-lens.iife.js")], VENDOR_FILES.map((f) => path.join(here, "..", "extension", "vendor", f))];
|
|
148
|
+
for (const files of candidates) {
|
|
147
149
|
try {
|
|
148
|
-
bundleCache = fs.readFileSync(
|
|
150
|
+
bundleCache = files.map((f) => fs.readFileSync(f, "utf8")).join("\n;\n");
|
|
149
151
|
return bundleCache;
|
|
150
152
|
} catch {
|
|
151
153
|
}
|
|
@@ -183,6 +185,7 @@ function expectWithinBudget(reports, budget) {
|
|
|
183
185
|
return assertWithinBudget(reports, budget);
|
|
184
186
|
}
|
|
185
187
|
|
|
188
|
+
exports.VENDOR_FILES = VENDOR_FILES;
|
|
186
189
|
exports.checkBudget = checkBudget;
|
|
187
190
|
exports.clearReports = clearReports;
|
|
188
191
|
exports.expectWithinBudget = expectWithinBudget;
|
package/dist/playwright.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../node_modules/tsup/assets/cjs_shims.js","../src/report.ts","../src/fixes.ts","../src/budget.ts","../src/playwright.ts"],"names":["dirname","fileURLToPath","join","readFileSync"],"mappings":"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","file":"playwright.cjs","sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import type { Change, CommitCause, CommitPriority, HookChange, HookSnapshot, Options, ParentInfo, RenderReport, RenderTrigger, SourceLocation } from './types';\n\nconst now = (): number =>\n typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();\n\nexport interface BuildInput {\n component: string;\n instanceId?: number;\n renderCount: number;\n prevProps: Record<string, unknown>;\n nextProps: Record<string, unknown>;\n propChanges: Change[];\n stateChanges?: Change[];\n hookChanges?: HookChange[];\n hookState?: HookSnapshot[];\n contexts?: { name: string; value: unknown }[];\n state?: Record<string, unknown>;\n parent?: ParentInfo | null;\n owner?: string | null;\n path?: string[];\n /** Default true: `useWhyRerender` and the console output assume the component decides on props alone. */\n memoized?: boolean;\n selfDuration?: number;\n treeDuration?: number;\n commitId?: number;\n commitPriority?: CommitPriority;\n source?: SourceLocation;\n updaters?: string[];\n commitCause?: CommitCause;\n afterCommit?: number;\n key?: string | null;\n}\n\n/** `useCart › useState#0` when custom hook names are known, else `useState #0`. */\nexport const hookLabel = (c: { hook: string; index: number; custom?: string[] }): string =>\n c.custom && c.custom.length ? `${c.custom.join(' › ')} › ${c.hook}#${c.index}` : `${c.hook} #${c.index}`;\n\n/** Store-specific advice for a `useSyncExternalStore` snapshot that is a new reference with equal contents. */\nexport function storeAdvice(custom: string[] | undefined): string {\n const chain = custom ?? [];\n if (chain.some((n) => /^useSelector$/.test(n)))\n return 'the Redux selector returns a new object on every call: return a stored slice, pass shallowEqual as the equality function, or memoize it with createSelector';\n if (chain.some((n) => /^useAppSelector$/.test(n)))\n return 'the selector returns a new object on every call: return a stored slice, pass shallowEqual, or memoize it with createSelector';\n if (chain.some((n) => /^use[A-Z]\\w*Store$/.test(n) || n === 'useStore' || n === 'useBoundStore'))\n return 'the store selector returns a new object on every call: select a primitive, or wrap the selector with useShallow (Zustand) / an equality function';\n return 'getSnapshot returns a new reference with the same contents: cache the snapshot in the store and return the same object while the data is unchanged';\n}\n\nconst isGenuine = (c: Change): boolean => c.kind === 'different' || c.kind === 'added' || c.kind === 'removed';\n\n/** `children` re-created in the parent's render: the classic memo-defeater, with its own advice. */\nconst isChildren = (change: Change): boolean => change.path === 'children';\n\nexport function fixFor(change: Change): string {\n if (isChildren(change) && (change.kind === 'element' || change.kind === 'deep-equal')) {\n return `lift the children out of the parent's render: memoize them with useMemo, hoist static elements to module scope, or render them from a component that does not re-render`;\n }\n switch (change.kind) {\n case 'function':\n return `wrap it in useCallback (or hoist it out of the parent's render)`;\n case 'element':\n return `memoize the element with useMemo, or pass it as children from a stable parent`;\n case 'deep-equal':\n return Array.isArray(change.next)\n ? `memoize the array with useMemo (same items, new reference)`\n : `memoize the object with useMemo, or hoist it to module scope if it is constant`;\n default:\n return '';\n }\n}\n\nfunction describe(change: Change): string {\n if (isChildren(change) && (change.kind === 'element' || change.kind === 'deep-equal')) {\n return `children are new React elements with the same types and props on every render of the parent`;\n }\n switch (change.kind) {\n case 'deep-equal':\n return `prop \"${change.path}\" is a new reference but deep-equal to the previous value`;\n case 'function':\n return `prop \"${change.path}\" is a new function instance on every render`;\n case 'element':\n return `prop \"${change.path}\" is a new React element with the same type and props`;\n case 'different':\n return `prop \"${change.path}\" changed`;\n case 'added':\n return `prop \"${change.path}\" was added`;\n case 'removed':\n return `prop \"${change.path}\" was removed`;\n }\n}\n\nexport function buildReport(input: BuildInput): RenderReport {\n const stateChanges = input.stateChanges ?? [];\n const hookChanges = input.hookChanges ?? [];\n const isStateHook = (h: HookChange): boolean => h.hook === 'useState' || h.hook === 'useReducer';\n const genuineProps = input.propChanges.some(isGenuine);\n const genuineState = stateChanges.some(isGenuine) || hookChanges.some((h) => isStateHook(h) && isGenuine(h));\n const genuineHooks = hookChanges.some((h) => !isStateHook(h) && isGenuine(h));\n const causes = [genuineProps && 'props', genuineState && 'state', genuineHooks && 'hooks'].filter(Boolean) as RenderTrigger[];\n\n let trigger: RenderTrigger;\n if (causes.length === 0) trigger = 'parent';\n else if (causes.length === 1) trigger = causes[0]!;\n else trigger = 'mixed';\n\n const avoidable = trigger === 'parent';\n const reasons: string[] = [];\n\n if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {\n const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : 'its parent re-rendered';\n reasons.push(`re-rendered with identical props because ${who}. Wrap \"${input.component}\" in React.memo (or extend PureComponent).`);\n } else if (avoidable && input.parent) {\n reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);\n }\n for (const c of input.propChanges) {\n const fix = fixFor(c);\n reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);\n }\n const memoized = input.memoized !== false;\n if (avoidable && !memoized && input.propChanges.length > 0) {\n reasons.push(\n `\"${input.component}\" is not memoized, so fixing the props alone will not stop this re-render: also wrap it in React.memo (or extend PureComponent).`,\n );\n }\n for (const c of stateChanges) {\n if (isGenuine(c)) reasons.push(`state \"${c.path}\" changed.`);\n else reasons.push(`setState was called with a value deep-equal to the current \"${c.path}\" (new reference, same contents).`);\n }\n for (const c of hookChanges) {\n if (c.hook === 'useContext' && isGenuine(c)) {\n const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : '';\n const keys =\n c.changedKeys && typeof c.totalKeys === 'number' && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys\n ? `: only ${c.changedKeys.map((k) => `\"${k}\"`).join(', ')} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read`\n : '';\n reasons.push(`${c.path} changed${where}${keys}.`);\n } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);\n else if (isStateHook(c))\n reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);\n else if (c.hook === 'useSyncExternalStore') reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: ${storeAdvice(c.custom)}.`);\n else reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: memoize the context/store value where it is produced.`);\n }\n if (input.commitCause === 'effect-after-commit') {\n const who = input.updaters && input.updaters.length ? input.updaters.map((u) => `<${u}>`).join(', ') : 'a component that rendered in it';\n reasons.push(`this commit was scheduled right after commit #${input.afterCommit ?? '?'} by ${who}: an effect there set state (effect → setState loop). Derive the value during render or compute it before setting state.`);\n } else if (input.commitCause === 'suspense-resolved') {\n reasons.push('this commit shows content that was suspended (a Suspense boundary resolved).');\n }\n\n const report: RenderReport = {\n component: input.component,\n instanceId: input.instanceId ?? 0,\n commitId: input.commitId ?? 0,\n renderCount: input.renderCount,\n trigger,\n avoidable,\n memoized,\n props: { prev: input.prevProps, next: input.nextProps },\n propChanges: input.propChanges,\n stateChanges,\n hookChanges,\n parent: input.parent ?? null,\n owner: input.owner ?? null,\n path: input.path ?? [],\n reasons,\n time: now(),\n };\n if (input.selfDuration !== undefined) report.selfDuration = input.selfDuration;\n if (input.treeDuration !== undefined) report.treeDuration = input.treeDuration;\n if (input.commitPriority) report.commitPriority = input.commitPriority;\n if (input.source) report.source = input.source;\n if (input.hookState) report.hookState = input.hookState;\n if (input.contexts) report.contexts = input.contexts;\n if (input.state) report.state = input.state;\n if (input.updaters && input.updaters.length) report.updaters = input.updaters;\n if (input.commitCause) report.commitCause = input.commitCause;\n if (input.afterCommit !== undefined) report.afterCommit = input.afterCommit;\n if (input.key !== undefined) report.key = input.key;\n return report;\n}\n\nfunction describeTrigger(t: RenderTrigger): string {\n switch (t) {\n case 'props':\n return 'its props changed';\n case 'state':\n return 'its state changed';\n case 'hooks':\n return 'a context or store it reads changed';\n case 'mixed':\n return 'its props and state changed';\n default:\n return 'its own parent re-rendered';\n }\n}\n\nconst KIND_LABEL: Record<Change['kind'], string> = {\n 'deep-equal': 'equal by value',\n function: 'new function',\n element: 'equal element',\n different: 'changed',\n added: 'added',\n removed: 'removed',\n};\n\nexport function summarize(report: RenderReport): string {\n const counts = new Map<string, number>();\n for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {\n counts.set(c.kind, (counts.get(c.kind) ?? 0) + 1);\n }\n const parts = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind as Change['kind']]}`);\n if (parts.length === 0) parts.push('no changes');\n return parts.join(', ');\n}\n\n/** Default notifier: prints a console group per report. */\nexport function printReport(report: RenderReport, options: Options): void {\n const c = options.console ?? console;\n const open = options.collapse === false ? c.group : c.groupCollapsed;\n const verdict = report.avoidable ? 'avoidable re-render' : `re-render (${report.trigger})`;\n open.call(c, `[rerender-lens] <${report.component}> ${verdict}: ${summarize(report)}`);\n for (const r of report.reasons) c.log(`- ${r}`);\n if (report.path.length) c.log(`at ${[...report.path, report.component].join(' > ')}`);\n for (const ch of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {\n c.log(`${ch.path} (${KIND_LABEL[ch.kind]})`, { prev: ch.prev, next: ch.next });\n }\n c.log('props', report.props);\n if (report.hookState && report.hookState.length) c.log('hooks', Object.fromEntries(report.hookState.map((h) => [h.path, h.value])));\n if (report.contexts && report.contexts.length) c.log('contexts', Object.fromEntries(report.contexts.map((x) => [x.name, x.value])));\n if (report.state) c.log('state', report.state);\n c.groupEnd();\n}\n","/**\n * Concrete fixes derived from reports, ranked by how many avoidable re-renders each one removes.\n * The same logic drives the extension's Fixes view; here it is typed on `RenderReport` for tests,\n * CI budgets and the CLI. Pure.\n */\nimport type { Change, HookChange, RenderReport } from './types';\nimport { storeAdvice } from './report';\n\nexport type FixKind = 'memo' | 'useCallback' | 'useMemo' | 'useMemoElement' | 'children' | 'contextValue' | 'splitContext' | 'storeSnapshot' | 'bailout';\n\nexport interface Fix {\n kind: FixKind;\n /** Where the change goes (the component that must be edited, or `<Ctx>.Provider`). */\n owner: string;\n /** The component whose re-render this removes. */\n target: string;\n prop: string | null;\n label: string;\n detail: string;\n}\n\nexport interface RankedFix extends Fix {\n key: string;\n /** Avoidable re-renders this fix removes. */\n count: number;\n components: Record<string, number>;\n}\n\nconst AVOIDABLE = new Set<string>(['deep-equal', 'function', 'element']);\nconst rootOf = (path: string): string => path.split(/[.[]/)[0] || path;\n\nexport function fixesFor(r: RenderReport): Fix[] {\n const out: Fix[] = [];\n const owner = r.owner || (r.parent && r.parent.name) || '?';\n const changes: Change[] = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];\n if (r.avoidable && (changes.length === 0 || r.memoized === false)) {\n out.push({\n kind: 'memo',\n owner: r.component,\n target: r.component,\n prop: null,\n label: `Wrap <${r.component}> in React.memo`,\n detail: changes.length === 0 ? `<${r.component}> re-rendered with identical props because <${(r.parent && r.parent.name) || 'its parent'}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`,\n });\n }\n for (const c of r.propChanges) {\n if (!AVOIDABLE.has(c.kind)) continue;\n const root = rootOf(c.path);\n if (root === 'children' && (c.kind === 'element' || c.kind === 'deep-equal')) {\n out.push({ kind: 'children', owner, target: r.component, prop: 'children', label: `memoize children of <${r.component}> in <${owner}>`, detail: `<${owner}> re-creates the children of <${r.component}> on every render.` });\n } else if (c.kind === 'function') {\n out.push({ kind: 'useCallback', owner, target: r.component, prop: root, label: `useCallback(${root}) in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new function on every render of <${owner}>.` });\n } else if (c.kind === 'element') {\n out.push({ kind: 'useMemoElement', owner, target: r.component, prop: root, label: `memoize element prop ${root} in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.` });\n } else {\n out.push({ kind: 'useMemo', owner, target: r.component, prop: root, label: `useMemo(${root}) in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new ${Array.isArray(c.next) ? 'array' : 'object'} with the same contents on every render of <${owner}>.` });\n }\n }\n for (const c of [...r.stateChanges, ...r.hookChanges] as (Change & Partial<HookChange>)[]) {\n const isContext = c.hook === 'useContext';\n const ctxName = isContext ? (/useContext\\((.*)\\)/.exec(c.path) || [])[1] || 'Context' : '';\n const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;\n if (isContext && c.kind === 'different' && c.changedKeys && typeof c.totalKeys === 'number' && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {\n out.push({\n kind: 'splitContext',\n owner: providerOwner || `${ctxName}.Provider`,\n target: r.component,\n prop: ctxName,\n label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ''}: only ${c.changedKeys.join(', ')} changed`,\n detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`,\n });\n continue;\n }\n if (!AVOIDABLE.has(c.kind)) continue;\n if (isContext) {\n out.push({ kind: 'contextValue', owner: providerOwner || `${ctxName}.Provider`, target: r.component, prop: ctxName, label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ''}`, detail: `${ctxName} produced a new value that is deep-equal to the previous one.` });\n } else if (c.hook === 'useSyncExternalStore') {\n const chain = c.custom || [];\n const redux = chain.some((n) => /^use(App)?Selector$/.test(n));\n const zustand = !redux && chain.some((n) => /^use[A-Z]\\w*Store$/.test(n) || n === 'useStore' || n === 'useBoundStore');\n out.push({ kind: 'storeSnapshot', owner: r.component, target: r.component, prop: c.path, label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`, detail: storeAdvice(c.custom) });\n } else {\n out.push({ kind: 'bailout', owner: r.component, target: r.component, prop: c.path, label: `bail out before setting ${c.path} in <${r.component}>`, detail: `${c.path} was set to a value deep-equal to the current one (new reference).` });\n }\n }\n return out;\n}\n\nexport const fixKey = (f: Fix): string => `${f.kind}|${f.owner}|${f.prop || f.target}`;\n\n/** Every fix across `reports`, most re-renders removed first. */\nexport function rankFixes(reports: RenderReport[]): RankedFix[] {\n const byKey = new Map<string, RankedFix>();\n for (const r of reports) {\n if (!r.avoidable) continue;\n for (const f of fixesFor(r)) {\n const key = fixKey(f);\n let agg = byKey.get(key);\n if (!agg) {\n agg = { ...f, key, count: 0, components: {} };\n byKey.set(key, agg);\n }\n agg.count++;\n agg.components[r.component] = (agg.components[r.component] || 0) + 1;\n }\n }\n return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));\n}\n\n/** Multi-line text for a test failure or a CI log. */\nexport function formatFixes(reports: RenderReport[], limit = 10): string {\n const ranked = rankFixes(reports);\n if (!ranked.length) return 'No avoidable re-renders.';\n const lines = ranked.slice(0, limit).map((f, i) => `${String(i + 1).padStart(2)}. ${f.label} (removes ${f.count}: ${Object.entries(f.components).map(([c, n]) => `<${c}>${n > 1 ? ' x' + n : ''}`).join(', ')})`);\n if (ranked.length > limit) lines.push(` … ${ranked.length - limit} more`);\n return lines.join('\\n');\n}\n","/**\n * Re-render budgets for CI: a JSON baseline of allowed avoidable re-renders per component, checked\n * against a collector's reports (like a bundle-size budget). Pure; reading and writing the file is\n * up to the test runner (`fs`), so this works in Node and in the browser.\n */\nimport type { RenderReport } from './types';\nimport { formatFixes } from './fixes';\n\n/** `{ \"*\": 0, \"ProductRow\": 3 }`: max avoidable re-renders per component; `*` is the default. */\nexport type Budget = Record<string, number>;\n\nexport interface BudgetViolation {\n component: string;\n avoidable: number;\n allowed: number;\n}\n\nexport interface BudgetResult {\n ok: boolean;\n violations: BudgetViolation[];\n /** Components with headroom left, so budgets can be tightened. */\n slack: { component: string; avoidable: number; allowed: number }[];\n counts: Record<string, number>;\n}\n\nexport function avoidableCounts(reports: RenderReport[]): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const r of reports) if (r.avoidable) counts[r.component] = (counts[r.component] || 0) + 1;\n return counts;\n}\n\nexport function checkBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n const b: Budget = typeof budget === 'number' ? { '*': budget } : budget;\n const fallback = typeof b['*'] === 'number' ? b['*'] : 0;\n const counts = avoidableCounts(reports);\n const violations: BudgetViolation[] = [];\n const slack: BudgetResult['slack'] = [];\n const names = new Set([...Object.keys(counts), ...Object.keys(b).filter((k) => k !== '*')]);\n for (const component of names) {\n const allowed = typeof b[component] === 'number' ? b[component]! : fallback;\n const avoidable = counts[component] || 0;\n if (avoidable > allowed) violations.push({ component, avoidable, allowed });\n else if (avoidable < allowed) slack.push({ component, avoidable, allowed });\n }\n violations.sort((x, y) => y.avoidable - y.allowed - (x.avoidable - x.allowed));\n return { ok: violations.length === 0, violations, slack, counts };\n}\n\n/** A budget that exactly matches the current counts (the baseline to commit). */\nexport function toBudget(reports: RenderReport[]): Budget {\n return { '*': 0, ...avoidableCounts(reports) };\n}\n\n/** Throws with the violations and the ranked fixes; use it from a test. */\nexport function assertWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n const result = checkBudget(reports, budget);\n if (result.ok) return result;\n const lines = result.violations.map((v) => ` <${v.component}>: ${v.avoidable} avoidable re-render${v.avoidable === 1 ? '' : 's'} (budget ${v.allowed})`);\n throw new Error(`Re-render budget exceeded:\\n${lines.join('\\n')}\\n\\nFixes, most impact first:\\n${formatFixes(reports)}`);\n}\n","/**\n * Playwright helper: run rerender-lens inside any page under test, then pull the reports and assert\n * a budget from the test. Works on production-like builds too (names may be minified there).\n *\n * import { installRerenderLens, pullReports, expectWithinBudget } from 'rerender-lens/playwright';\n * await installRerenderLens(page, { trackAllMemoized: true }); // before page.goto\n * await page.goto('/');\n * ...interact...\n * expectWithinBudget(await pullReports(page), { '*': 0, ProductRow: 3 });\n *\n * The library bundle is injected with `page.addInitScript` at document start, exactly like the\n * extension's injection mode, so `init` runs before react-dom.\n */\nimport { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { RenderReport } from './types';\nimport type { SerializableOptions } from './devtools';\nimport { assertWithinBudget, checkBudget, type Budget, type BudgetResult } from './budget';\nimport { rankFixes, formatFixes, type RankedFix } from './fixes';\n\n/** The subset of Playwright's `Page` used here (keeps `@playwright/test` an optional peer). */\nexport interface PageLike {\n addInitScript(script: string | { content?: string }): Promise<void>;\n evaluate<R>(fn: string | ((arg: unknown) => R), arg?: unknown): Promise<R>;\n}\n\nexport interface InstallOptions extends SerializableOptions {\n /** Forward to a relay (`npx rerender-lens panel`) to watch the test in the panel. */\n relay?: string;\n /** Print to the page console (default false in tests). */\n silent?: boolean;\n /** Library source to inject instead of the packaged IIFE bundle (must define `window.RerenderLens`). */\n bundle?: string;\n}\n\nlet bundleCache: string | null = null;\n\n/** The IIFE bundle (`window.RerenderLens`) the extension injects; shipped as `dist/rerender-lens.iife.js`. */\nexport function libraryBundle(): string {\n if (bundleCache) return bundleCache;\n const here = dirname(fileURLToPath(import.meta.url));\n for (const file of [join(here, 'rerender-lens.iife.js'), join(here, '..', 'extension', 'vendor', 'rerender-lens.js')]) {\n try {\n bundleCache = readFileSync(file, 'utf8');\n return bundleCache;\n } catch {\n /* next */\n }\n }\n throw new Error('rerender-lens: library bundle not found (run `npm run build` in the rerender-lens repo, or reinstall the package)');\n}\n\n/** Script that starts the library with `options` before any page script runs. */\nexport function installScript(options: InstallOptions = {}): string {\n const { relay, bundle, ...rest } = options;\n const opts: InstallOptions = { trackAllMemoized: true, silent: true, ...rest };\n return `${bundle ?? libraryBundle()}\\n;(function(){\\n if (window.__RERENDER_LENS_DEVTOOLS__) return;\\n var L = window.RerenderLens;\\n L.ensureDevtoolsHook();\\n var o = ${JSON.stringify(opts)};\\n o.notifier = L.createDevtoolsNotifier(${relay ? JSON.stringify({ relay }) : '{}'});\\n if (o.include) o.include = o.include.map(function (m) { var r = /^\\\\/(.+)\\\\/([a-z]*)$/.exec(m); return r ? new RegExp(r[1], r[2]) : m; });\\n if (o.exclude) o.exclude = o.exclude.map(function (m) { var r = /^\\\\/(.+)\\\\/([a-z]*)$/.exec(m); return r ? new RegExp(r[1], r[2]) : m; });\\n L.init(o);\\n window.__RERENDER_LENS_PLAYWRIGHT__ = true;\\n})();`;\n}\n\n/** Call before `page.goto`. */\nexport async function installRerenderLens(page: PageLike, options: InstallOptions = {}): Promise<void> {\n await page.addInitScript({ content: installScript(options) });\n}\n\n/** Every report buffered in the page so far (serialized: functions are `ƒ name`, elements `{ $type: 'element' }`). */\nexport async function pullReports(page: PageLike): Promise<RenderReport[]> {\n const result = await page.evaluate<{ reports: unknown[] } | null>('(function(){var b=window.__RERENDER_LENS_DEVTOOLS__;return b&&b.pull?b.pull(0):null})()');\n if (!result) throw new Error('rerender-lens is not running in the page: call installRerenderLens(page) before page.goto');\n return result.reports as RenderReport[];\n}\n\n/** Drop the page buffer (between scenarios). */\nexport async function clearReports(page: PageLike): Promise<void> {\n await page.evaluate('(function(){var b=window.__RERENDER_LENS_DEVTOOLS__;if(b)b.clear();})()');\n}\n\nexport { checkBudget, rankFixes, formatFixes };\nexport type { Budget, BudgetResult, RankedFix };\n\n/** Throws with the violations and ranked fixes, like `collector.assertWithinBudget`. */\nexport function expectWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n return assertWithinBudget(reports, budget);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../node_modules/tsup/assets/cjs_shims.js","../src/report.ts","../src/fixes.ts","../src/budget.ts","../src/playwright.ts"],"names":["dirname","fileURLToPath","join","readFileSync"],"mappings":"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","file":"playwright.cjs","sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import type { Change, CommitCause, CommitPriority, HookChange, HookSnapshot, Options, ParentInfo, RenderReport, RenderTrigger, SourceLocation } from './types';\n\nconst now = (): number =>\n typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();\n\nexport interface BuildInput {\n component: string;\n instanceId?: number;\n renderCount: number;\n prevProps: Record<string, unknown>;\n nextProps: Record<string, unknown>;\n propChanges: Change[];\n stateChanges?: Change[];\n hookChanges?: HookChange[];\n hookState?: HookSnapshot[];\n contexts?: { name: string; value: unknown }[];\n state?: Record<string, unknown>;\n parent?: ParentInfo | null;\n owner?: string | null;\n path?: string[];\n /** Default true: `useWhyRerender` and the console output assume the component decides on props alone. */\n memoized?: boolean;\n selfDuration?: number;\n treeDuration?: number;\n commitId?: number;\n commitPriority?: CommitPriority;\n source?: SourceLocation;\n updaters?: string[];\n commitCause?: CommitCause;\n afterCommit?: number;\n key?: string | null;\n}\n\n/** `useCart › useState#0` when custom hook names are known, else `useState #0`. */\nexport const hookLabel = (c: { hook: string; index: number; custom?: string[] }): string =>\n c.custom && c.custom.length ? `${c.custom.join(' › ')} › ${c.hook}#${c.index}` : `${c.hook} #${c.index}`;\n\n/** Store-specific advice for a `useSyncExternalStore` snapshot that is a new reference with equal contents. */\nexport function storeAdvice(custom: string[] | undefined): string {\n const chain = custom ?? [];\n if (chain.some((n) => /^useSelector$/.test(n)))\n return 'the Redux selector returns a new object on every call: return a stored slice, pass shallowEqual as the equality function, or memoize it with createSelector';\n if (chain.some((n) => /^useAppSelector$/.test(n)))\n return 'the selector returns a new object on every call: return a stored slice, pass shallowEqual, or memoize it with createSelector';\n if (chain.some((n) => /^use[A-Z]\\w*Store$/.test(n) || n === 'useStore' || n === 'useBoundStore'))\n return 'the store selector returns a new object on every call: select a primitive, or wrap the selector with useShallow (Zustand) / an equality function';\n return 'getSnapshot returns a new reference with the same contents: cache the snapshot in the store and return the same object while the data is unchanged';\n}\n\nconst isGenuine = (c: Change): boolean => c.kind === 'different' || c.kind === 'added' || c.kind === 'removed';\n\n/** `children` re-created in the parent's render: the classic memo-defeater, with its own advice. */\nconst isChildren = (change: Change): boolean => change.path === 'children';\n\nexport function fixFor(change: Change): string {\n if (isChildren(change) && (change.kind === 'element' || change.kind === 'deep-equal')) {\n return `lift the children out of the parent's render: memoize them with useMemo, hoist static elements to module scope, or render them from a component that does not re-render`;\n }\n switch (change.kind) {\n case 'function':\n return `wrap it in useCallback (or hoist it out of the parent's render)`;\n case 'element':\n return `memoize the element with useMemo, or pass it as children from a stable parent`;\n case 'deep-equal':\n return Array.isArray(change.next)\n ? `memoize the array with useMemo (same items, new reference)`\n : `memoize the object with useMemo, or hoist it to module scope if it is constant`;\n default:\n return '';\n }\n}\n\nfunction describe(change: Change): string {\n if (isChildren(change) && (change.kind === 'element' || change.kind === 'deep-equal')) {\n return `children are new React elements with the same types and props on every render of the parent`;\n }\n switch (change.kind) {\n case 'deep-equal':\n return `prop \"${change.path}\" is a new reference but deep-equal to the previous value`;\n case 'function':\n return `prop \"${change.path}\" is a new function instance on every render`;\n case 'element':\n return `prop \"${change.path}\" is a new React element with the same type and props`;\n case 'different':\n return `prop \"${change.path}\" changed`;\n case 'added':\n return `prop \"${change.path}\" was added`;\n case 'removed':\n return `prop \"${change.path}\" was removed`;\n }\n}\n\nexport function buildReport(input: BuildInput): RenderReport {\n const stateChanges = input.stateChanges ?? [];\n const hookChanges = input.hookChanges ?? [];\n const isStateHook = (h: HookChange): boolean => h.hook === 'useState' || h.hook === 'useReducer';\n const genuineProps = input.propChanges.some(isGenuine);\n const genuineState = stateChanges.some(isGenuine) || hookChanges.some((h) => isStateHook(h) && isGenuine(h));\n const genuineHooks = hookChanges.some((h) => !isStateHook(h) && isGenuine(h));\n const causes = [genuineProps && 'props', genuineState && 'state', genuineHooks && 'hooks'].filter(Boolean) as RenderTrigger[];\n\n let trigger: RenderTrigger;\n if (causes.length === 0) trigger = 'parent';\n else if (causes.length === 1) trigger = causes[0]!;\n else trigger = 'mixed';\n\n const avoidable = trigger === 'parent';\n const reasons: string[] = [];\n\n if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {\n const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : 'its parent re-rendered';\n reasons.push(`re-rendered with identical props because ${who}. Wrap \"${input.component}\" in React.memo (or extend PureComponent).`);\n } else if (avoidable && input.parent) {\n reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);\n }\n for (const c of input.propChanges) {\n const fix = fixFor(c);\n reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);\n }\n const memoized = input.memoized !== false;\n if (avoidable && !memoized && input.propChanges.length > 0) {\n reasons.push(\n `\"${input.component}\" is not memoized, so fixing the props alone will not stop this re-render: also wrap it in React.memo (or extend PureComponent).`,\n );\n }\n for (const c of stateChanges) {\n if (isGenuine(c)) reasons.push(`state \"${c.path}\" changed.`);\n else reasons.push(`setState was called with a value deep-equal to the current \"${c.path}\" (new reference, same contents).`);\n }\n for (const c of hookChanges) {\n if (c.hook === 'useContext' && isGenuine(c)) {\n const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : '';\n const keys =\n c.changedKeys && typeof c.totalKeys === 'number' && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys\n ? `: only ${c.changedKeys.map((k) => `\"${k}\"`).join(', ')} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read`\n : '';\n reasons.push(`${c.path} changed${where}${keys}.`);\n } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);\n else if (isStateHook(c))\n reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);\n else if (c.hook === 'useSyncExternalStore') reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: ${storeAdvice(c.custom)}.`);\n else reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: memoize the context/store value where it is produced.`);\n }\n if (input.commitCause === 'effect-after-commit') {\n const who = input.updaters && input.updaters.length ? input.updaters.map((u) => `<${u}>`).join(', ') : 'a component that rendered in it';\n reasons.push(`this commit was scheduled right after commit #${input.afterCommit ?? '?'} by ${who}: an effect there set state (effect → setState loop). Derive the value during render or compute it before setting state.`);\n } else if (input.commitCause === 'suspense-resolved') {\n reasons.push('this commit shows content that was suspended (a Suspense boundary resolved).');\n }\n\n const report: RenderReport = {\n component: input.component,\n instanceId: input.instanceId ?? 0,\n commitId: input.commitId ?? 0,\n renderCount: input.renderCount,\n trigger,\n avoidable,\n memoized,\n props: { prev: input.prevProps, next: input.nextProps },\n propChanges: input.propChanges,\n stateChanges,\n hookChanges,\n parent: input.parent ?? null,\n owner: input.owner ?? null,\n path: input.path ?? [],\n reasons,\n time: now(),\n };\n if (input.selfDuration !== undefined) report.selfDuration = input.selfDuration;\n if (input.treeDuration !== undefined) report.treeDuration = input.treeDuration;\n if (input.commitPriority) report.commitPriority = input.commitPriority;\n if (input.source) report.source = input.source;\n if (input.hookState) report.hookState = input.hookState;\n if (input.contexts) report.contexts = input.contexts;\n if (input.state) report.state = input.state;\n if (input.updaters && input.updaters.length) report.updaters = input.updaters;\n if (input.commitCause) report.commitCause = input.commitCause;\n if (input.afterCommit !== undefined) report.afterCommit = input.afterCommit;\n if (input.key !== undefined) report.key = input.key;\n return report;\n}\n\nfunction describeTrigger(t: RenderTrigger): string {\n switch (t) {\n case 'props':\n return 'its props changed';\n case 'state':\n return 'its state changed';\n case 'hooks':\n return 'a context or store it reads changed';\n case 'mixed':\n return 'its props and state changed';\n default:\n return 'its own parent re-rendered';\n }\n}\n\nconst KIND_LABEL: Record<Change['kind'], string> = {\n 'deep-equal': 'equal by value',\n function: 'new function',\n element: 'equal element',\n different: 'changed',\n added: 'added',\n removed: 'removed',\n};\n\nexport function summarize(report: RenderReport): string {\n const counts = new Map<string, number>();\n for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {\n counts.set(c.kind, (counts.get(c.kind) ?? 0) + 1);\n }\n const parts = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind as Change['kind']]}`);\n if (parts.length === 0) parts.push('no changes');\n return parts.join(', ');\n}\n\n/** Default notifier: prints a console group per report. */\nexport function printReport(report: RenderReport, options: Options): void {\n const c = options.console ?? console;\n const open = options.collapse === false ? c.group : c.groupCollapsed;\n const verdict = report.avoidable ? 'avoidable re-render' : `re-render (${report.trigger})`;\n open.call(c, `[rerender-lens] <${report.component}> ${verdict}: ${summarize(report)}`);\n for (const r of report.reasons) c.log(`- ${r}`);\n if (report.path.length) c.log(`at ${[...report.path, report.component].join(' > ')}`);\n for (const ch of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {\n c.log(`${ch.path} (${KIND_LABEL[ch.kind]})`, { prev: ch.prev, next: ch.next });\n }\n c.log('props', report.props);\n if (report.hookState && report.hookState.length) c.log('hooks', Object.fromEntries(report.hookState.map((h) => [h.path, h.value])));\n if (report.contexts && report.contexts.length) c.log('contexts', Object.fromEntries(report.contexts.map((x) => [x.name, x.value])));\n if (report.state) c.log('state', report.state);\n c.groupEnd();\n}\n","/**\n * Concrete fixes derived from reports, ranked by how many avoidable re-renders each one removes.\n * The same logic drives the extension's Fixes view; here it is typed on `RenderReport` for tests,\n * CI budgets and the CLI. Pure.\n */\nimport type { Change, HookChange, RenderReport } from './types';\nimport { storeAdvice } from './report';\n\nexport type FixKind = 'memo' | 'useCallback' | 'useMemo' | 'useMemoElement' | 'children' | 'contextValue' | 'splitContext' | 'storeSnapshot' | 'bailout';\n\nexport interface Fix {\n kind: FixKind;\n /** Where the change goes (the component that must be edited, or `<Ctx>.Provider`). */\n owner: string;\n /** The component whose re-render this removes. */\n target: string;\n prop: string | null;\n label: string;\n detail: string;\n}\n\nexport interface RankedFix extends Fix {\n key: string;\n /** Avoidable re-renders this fix removes. */\n count: number;\n components: Record<string, number>;\n}\n\nconst AVOIDABLE = new Set<string>(['deep-equal', 'function', 'element']);\nconst rootOf = (path: string): string => path.split(/[.[]/)[0] || path;\n\nexport function fixesFor(r: RenderReport): Fix[] {\n const out: Fix[] = [];\n const owner = r.owner || (r.parent && r.parent.name) || '?';\n const changes: Change[] = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];\n if (r.avoidable && (changes.length === 0 || r.memoized === false)) {\n out.push({\n kind: 'memo',\n owner: r.component,\n target: r.component,\n prop: null,\n label: `Wrap <${r.component}> in React.memo`,\n detail: changes.length === 0 ? `<${r.component}> re-rendered with identical props because <${(r.parent && r.parent.name) || 'its parent'}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`,\n });\n }\n for (const c of r.propChanges) {\n if (!AVOIDABLE.has(c.kind)) continue;\n const root = rootOf(c.path);\n if (root === 'children' && (c.kind === 'element' || c.kind === 'deep-equal')) {\n out.push({ kind: 'children', owner, target: r.component, prop: 'children', label: `memoize children of <${r.component}> in <${owner}>`, detail: `<${owner}> re-creates the children of <${r.component}> on every render.` });\n } else if (c.kind === 'function') {\n out.push({ kind: 'useCallback', owner, target: r.component, prop: root, label: `useCallback(${root}) in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new function on every render of <${owner}>.` });\n } else if (c.kind === 'element') {\n out.push({ kind: 'useMemoElement', owner, target: r.component, prop: root, label: `memoize element prop ${root} in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.` });\n } else {\n out.push({ kind: 'useMemo', owner, target: r.component, prop: root, label: `useMemo(${root}) in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new ${Array.isArray(c.next) ? 'array' : 'object'} with the same contents on every render of <${owner}>.` });\n }\n }\n for (const c of [...r.stateChanges, ...r.hookChanges] as (Change & Partial<HookChange>)[]) {\n const isContext = c.hook === 'useContext';\n const ctxName = isContext ? (/useContext\\((.*)\\)/.exec(c.path) || [])[1] || 'Context' : '';\n const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;\n if (isContext && c.kind === 'different' && c.changedKeys && typeof c.totalKeys === 'number' && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {\n out.push({\n kind: 'splitContext',\n owner: providerOwner || `${ctxName}.Provider`,\n target: r.component,\n prop: ctxName,\n label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ''}: only ${c.changedKeys.join(', ')} changed`,\n detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`,\n });\n continue;\n }\n if (!AVOIDABLE.has(c.kind)) continue;\n if (isContext) {\n out.push({ kind: 'contextValue', owner: providerOwner || `${ctxName}.Provider`, target: r.component, prop: ctxName, label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ''}`, detail: `${ctxName} produced a new value that is deep-equal to the previous one.` });\n } else if (c.hook === 'useSyncExternalStore') {\n const chain = c.custom || [];\n const redux = chain.some((n) => /^use(App)?Selector$/.test(n));\n const zustand = !redux && chain.some((n) => /^use[A-Z]\\w*Store$/.test(n) || n === 'useStore' || n === 'useBoundStore');\n out.push({ kind: 'storeSnapshot', owner: r.component, target: r.component, prop: c.path, label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`, detail: storeAdvice(c.custom) });\n } else {\n out.push({ kind: 'bailout', owner: r.component, target: r.component, prop: c.path, label: `bail out before setting ${c.path} in <${r.component}>`, detail: `${c.path} was set to a value deep-equal to the current one (new reference).` });\n }\n }\n return out;\n}\n\nexport const fixKey = (f: Fix): string => `${f.kind}|${f.owner}|${f.prop || f.target}`;\n\n/** Every fix across `reports`, most re-renders removed first. */\nexport function rankFixes(reports: RenderReport[]): RankedFix[] {\n const byKey = new Map<string, RankedFix>();\n for (const r of reports) {\n if (!r.avoidable) continue;\n for (const f of fixesFor(r)) {\n const key = fixKey(f);\n let agg = byKey.get(key);\n if (!agg) {\n agg = { ...f, key, count: 0, components: {} };\n byKey.set(key, agg);\n }\n agg.count++;\n agg.components[r.component] = (agg.components[r.component] || 0) + 1;\n }\n }\n return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));\n}\n\n/** Multi-line text for a test failure or a CI log. */\nexport function formatFixes(reports: RenderReport[], limit = 10): string {\n const ranked = rankFixes(reports);\n if (!ranked.length) return 'No avoidable re-renders.';\n const lines = ranked.slice(0, limit).map((f, i) => `${String(i + 1).padStart(2)}. ${f.label} (removes ${f.count}: ${Object.entries(f.components).map(([c, n]) => `<${c}>${n > 1 ? ' x' + n : ''}`).join(', ')})`);\n if (ranked.length > limit) lines.push(` … ${ranked.length - limit} more`);\n return lines.join('\\n');\n}\n","/**\n * Re-render budgets for CI: a JSON baseline of allowed avoidable re-renders per component, checked\n * against a collector's reports (like a bundle-size budget). Pure; reading and writing the file is\n * up to the test runner (`fs`), so this works in Node and in the browser.\n */\nimport type { RenderReport } from './types';\nimport { formatFixes } from './fixes';\n\n/** `{ \"*\": 0, \"ProductRow\": 3 }`: max avoidable re-renders per component; `*` is the default. */\nexport type Budget = Record<string, number>;\n\nexport interface BudgetViolation {\n component: string;\n avoidable: number;\n allowed: number;\n}\n\nexport interface BudgetResult {\n ok: boolean;\n violations: BudgetViolation[];\n /** Components with headroom left, so budgets can be tightened. */\n slack: { component: string; avoidable: number; allowed: number }[];\n counts: Record<string, number>;\n}\n\nexport function avoidableCounts(reports: RenderReport[]): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const r of reports) if (r.avoidable) counts[r.component] = (counts[r.component] || 0) + 1;\n return counts;\n}\n\nexport function checkBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n const b: Budget = typeof budget === 'number' ? { '*': budget } : budget;\n const fallback = typeof b['*'] === 'number' ? b['*'] : 0;\n const counts = avoidableCounts(reports);\n const violations: BudgetViolation[] = [];\n const slack: BudgetResult['slack'] = [];\n const names = new Set([...Object.keys(counts), ...Object.keys(b).filter((k) => k !== '*')]);\n for (const component of names) {\n const allowed = typeof b[component] === 'number' ? b[component]! : fallback;\n const avoidable = counts[component] || 0;\n if (avoidable > allowed) violations.push({ component, avoidable, allowed });\n else if (avoidable < allowed) slack.push({ component, avoidable, allowed });\n }\n violations.sort((x, y) => y.avoidable - y.allowed - (x.avoidable - x.allowed));\n return { ok: violations.length === 0, violations, slack, counts };\n}\n\n/** A budget that exactly matches the current counts (the baseline to commit). */\nexport function toBudget(reports: RenderReport[]): Budget {\n return { '*': 0, ...avoidableCounts(reports) };\n}\n\n/** Throws with the violations and the ranked fixes; use it from a test. */\nexport function assertWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n const result = checkBudget(reports, budget);\n if (result.ok) return result;\n const lines = result.violations.map((v) => ` <${v.component}>: ${v.avoidable} avoidable re-render${v.avoidable === 1 ? '' : 's'} (budget ${v.allowed})`);\n throw new Error(`Re-render budget exceeded:\\n${lines.join('\\n')}\\n\\nFixes, most impact first:\\n${formatFixes(reports)}`);\n}\n","/**\n * Playwright helper: run rerender-lens inside any page under test, then pull the reports and assert\n * a budget from the test. Works on production-like builds too (names may be minified there).\n *\n * import { installRerenderLens, pullReports, expectWithinBudget } from 'rerender-lens/playwright';\n * await installRerenderLens(page, { trackAllMemoized: true }); // before page.goto\n * await page.goto('/');\n * ...interact...\n * expectWithinBudget(await pullReports(page), { '*': 0, ProductRow: 3 });\n *\n * The library bundle is injected with `page.addInitScript` at document start, exactly like the\n * extension's injection mode, so `init` runs before react-dom.\n */\nimport { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { RenderReport } from './types';\nimport type { SerializableOptions } from './devtools';\nimport { assertWithinBudget, checkBudget, type Budget, type BudgetResult } from './budget';\nimport { rankFixes, formatFixes, type RankedFix } from './fixes';\n\n/** The subset of Playwright's `Page` used here (keeps `@playwright/test` an optional peer). */\nexport interface PageLike {\n addInitScript(script: string | { content?: string }): Promise<void>;\n evaluate<R>(fn: string | ((arg: unknown) => R), arg?: unknown): Promise<R>;\n}\n\nexport interface InstallOptions extends SerializableOptions {\n /** Forward to a relay (`npx rerender-lens panel`) to watch the test in the panel. */\n relay?: string;\n /** Print to the page console (default false in tests). */\n silent?: boolean;\n /** Library source to inject instead of the packaged IIFE bundle (must define `window.RerenderLens`). */\n bundle?: string;\n}\n\nlet bundleCache: string | null = null;\n\n/** The vendor scripts the extension injects, in load order (see scripts/build-vendor.mjs). */\nexport const VENDOR_FILES = ['rerender-lens.core.js', 'rerender-lens.engine.js', 'rerender-lens.js'];\n\n/** The library as one script (`window.RerenderLens`): `dist/rerender-lens.iife.js`, or the vendor parts joined in this repo. */\nexport function libraryBundle(): string {\n if (bundleCache) return bundleCache;\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [[join(here, 'rerender-lens.iife.js')], VENDOR_FILES.map((f) => join(here, '..', 'extension', 'vendor', f))];\n for (const files of candidates) {\n try {\n bundleCache = files.map((f) => readFileSync(f, 'utf8')).join('\\n;\\n');\n return bundleCache;\n } catch {\n /* next */\n }\n }\n throw new Error('rerender-lens: library bundle not found (run `npm run build` in the rerender-lens repo, or reinstall the package)');\n}\n\n/** Script that starts the library with `options` before any page script runs. */\nexport function installScript(options: InstallOptions = {}): string {\n const { relay, bundle, ...rest } = options;\n const opts: InstallOptions = { trackAllMemoized: true, silent: true, ...rest };\n return `${bundle ?? libraryBundle()}\\n;(function(){\\n if (window.__RERENDER_LENS_DEVTOOLS__) return;\\n var L = window.RerenderLens;\\n L.ensureDevtoolsHook();\\n var o = ${JSON.stringify(opts)};\\n o.notifier = L.createDevtoolsNotifier(${relay ? JSON.stringify({ relay }) : '{}'});\\n if (o.include) o.include = o.include.map(function (m) { var r = /^\\\\/(.+)\\\\/([a-z]*)$/.exec(m); return r ? new RegExp(r[1], r[2]) : m; });\\n if (o.exclude) o.exclude = o.exclude.map(function (m) { var r = /^\\\\/(.+)\\\\/([a-z]*)$/.exec(m); return r ? new RegExp(r[1], r[2]) : m; });\\n L.init(o);\\n window.__RERENDER_LENS_PLAYWRIGHT__ = true;\\n})();`;\n}\n\n/** Call before `page.goto`. */\nexport async function installRerenderLens(page: PageLike, options: InstallOptions = {}): Promise<void> {\n await page.addInitScript({ content: installScript(options) });\n}\n\n/** Every report buffered in the page so far (serialized: functions are `ƒ name`, elements `{ $type: 'element' }`). */\nexport async function pullReports(page: PageLike): Promise<RenderReport[]> {\n const result = await page.evaluate<{ reports: unknown[] } | null>('(function(){var b=window.__RERENDER_LENS_DEVTOOLS__;return b&&b.pull?b.pull(0):null})()');\n if (!result) throw new Error('rerender-lens is not running in the page: call installRerenderLens(page) before page.goto');\n return result.reports as RenderReport[];\n}\n\n/** Drop the page buffer (between scenarios). */\nexport async function clearReports(page: PageLike): Promise<void> {\n await page.evaluate('(function(){var b=window.__RERENDER_LENS_DEVTOOLS__;if(b)b.clear();})()');\n}\n\nexport { checkBudget, rankFixes, formatFixes };\nexport type { Budget, BudgetResult, RankedFix };\n\n/** Throws with the violations and ranked fixes, like `collector.assertWithinBudget`. */\nexport function expectWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n return assertWithinBudget(reports, budget);\n}\n"]}
|
package/dist/playwright.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { R as RenderReport } from './types-BzUEVkxJ.cjs';
|
|
2
|
-
import { S as SerializableOptions } from './devtools-
|
|
2
|
+
import { S as SerializableOptions } from './devtools-BkCct3cJ.cjs';
|
|
3
3
|
import { B as Budget, a as BudgetResult } from './budget-COBu7jBU.cjs';
|
|
4
4
|
export { R as RankedFix, c as checkBudget, f as formatFixes, r as rankFixes } from './budget-COBu7jBU.cjs';
|
|
5
5
|
|
|
@@ -18,7 +18,9 @@ interface InstallOptions extends SerializableOptions {
|
|
|
18
18
|
/** Library source to inject instead of the packaged IIFE bundle (must define `window.RerenderLens`). */
|
|
19
19
|
bundle?: string;
|
|
20
20
|
}
|
|
21
|
-
/** The
|
|
21
|
+
/** The vendor scripts the extension injects, in load order (see scripts/build-vendor.mjs). */
|
|
22
|
+
declare const VENDOR_FILES: string[];
|
|
23
|
+
/** The library as one script (`window.RerenderLens`): `dist/rerender-lens.iife.js`, or the vendor parts joined in this repo. */
|
|
22
24
|
declare function libraryBundle(): string;
|
|
23
25
|
/** Script that starts the library with `options` before any page script runs. */
|
|
24
26
|
declare function installScript(options?: InstallOptions): string;
|
|
@@ -32,4 +34,4 @@ declare function clearReports(page: PageLike): Promise<void>;
|
|
|
32
34
|
/** Throws with the violations and ranked fixes, like `collector.assertWithinBudget`. */
|
|
33
35
|
declare function expectWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult;
|
|
34
36
|
|
|
35
|
-
export { Budget, BudgetResult, type InstallOptions, type PageLike, clearReports, expectWithinBudget, installRerenderLens, installScript, libraryBundle, pullReports };
|
|
37
|
+
export { Budget, BudgetResult, type InstallOptions, type PageLike, VENDOR_FILES, clearReports, expectWithinBudget, installRerenderLens, installScript, libraryBundle, pullReports };
|
package/dist/playwright.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { R as RenderReport } from './types-BzUEVkxJ.js';
|
|
2
|
-
import { S as SerializableOptions } from './devtools-
|
|
2
|
+
import { S as SerializableOptions } from './devtools-CZebzpF6.js';
|
|
3
3
|
import { B as Budget, a as BudgetResult } from './budget-LkNjGtRc.js';
|
|
4
4
|
export { R as RankedFix, c as checkBudget, f as formatFixes, r as rankFixes } from './budget-LkNjGtRc.js';
|
|
5
5
|
|
|
@@ -18,7 +18,9 @@ interface InstallOptions extends SerializableOptions {
|
|
|
18
18
|
/** Library source to inject instead of the packaged IIFE bundle (must define `window.RerenderLens`). */
|
|
19
19
|
bundle?: string;
|
|
20
20
|
}
|
|
21
|
-
/** The
|
|
21
|
+
/** The vendor scripts the extension injects, in load order (see scripts/build-vendor.mjs). */
|
|
22
|
+
declare const VENDOR_FILES: string[];
|
|
23
|
+
/** The library as one script (`window.RerenderLens`): `dist/rerender-lens.iife.js`, or the vendor parts joined in this repo. */
|
|
22
24
|
declare function libraryBundle(): string;
|
|
23
25
|
/** Script that starts the library with `options` before any page script runs. */
|
|
24
26
|
declare function installScript(options?: InstallOptions): string;
|
|
@@ -32,4 +34,4 @@ declare function clearReports(page: PageLike): Promise<void>;
|
|
|
32
34
|
/** Throws with the violations and ranked fixes, like `collector.assertWithinBudget`. */
|
|
33
35
|
declare function expectWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult;
|
|
34
36
|
|
|
35
|
-
export { Budget, BudgetResult, type InstallOptions, type PageLike, clearReports, expectWithinBudget, installRerenderLens, installScript, libraryBundle, pullReports };
|
|
37
|
+
export { Budget, BudgetResult, type InstallOptions, type PageLike, VENDOR_FILES, clearReports, expectWithinBudget, installRerenderLens, installScript, libraryBundle, pullReports };
|
package/dist/playwright.js
CHANGED
|
@@ -136,12 +136,14 @@ ${formatFixes(reports)}`);
|
|
|
136
136
|
|
|
137
137
|
// src/playwright.ts
|
|
138
138
|
var bundleCache = null;
|
|
139
|
+
var VENDOR_FILES = ["rerender-lens.core.js", "rerender-lens.engine.js", "rerender-lens.js"];
|
|
139
140
|
function libraryBundle() {
|
|
140
141
|
if (bundleCache) return bundleCache;
|
|
141
142
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
142
|
-
|
|
143
|
+
const candidates = [[join(here, "rerender-lens.iife.js")], VENDOR_FILES.map((f) => join(here, "..", "extension", "vendor", f))];
|
|
144
|
+
for (const files of candidates) {
|
|
143
145
|
try {
|
|
144
|
-
bundleCache = readFileSync(
|
|
146
|
+
bundleCache = files.map((f) => readFileSync(f, "utf8")).join("\n;\n");
|
|
145
147
|
return bundleCache;
|
|
146
148
|
} catch {
|
|
147
149
|
}
|
|
@@ -179,6 +181,6 @@ function expectWithinBudget(reports, budget) {
|
|
|
179
181
|
return assertWithinBudget(reports, budget);
|
|
180
182
|
}
|
|
181
183
|
|
|
182
|
-
export { checkBudget, clearReports, expectWithinBudget, formatFixes, installRerenderLens, installScript, libraryBundle, pullReports, rankFixes };
|
|
184
|
+
export { VENDOR_FILES, checkBudget, clearReports, expectWithinBudget, formatFixes, installRerenderLens, installScript, libraryBundle, pullReports, rankFixes };
|
|
183
185
|
//# sourceMappingURL=playwright.js.map
|
|
184
186
|
//# sourceMappingURL=playwright.js.map
|
package/dist/playwright.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/report.ts","../src/fixes.ts","../src/budget.ts","../src/playwright.ts"],"names":[],"mappings":"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","file":"playwright.js","sourcesContent":["import type { Change, CommitCause, CommitPriority, HookChange, HookSnapshot, Options, ParentInfo, RenderReport, RenderTrigger, SourceLocation } from './types';\n\nconst now = (): number =>\n typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();\n\nexport interface BuildInput {\n component: string;\n instanceId?: number;\n renderCount: number;\n prevProps: Record<string, unknown>;\n nextProps: Record<string, unknown>;\n propChanges: Change[];\n stateChanges?: Change[];\n hookChanges?: HookChange[];\n hookState?: HookSnapshot[];\n contexts?: { name: string; value: unknown }[];\n state?: Record<string, unknown>;\n parent?: ParentInfo | null;\n owner?: string | null;\n path?: string[];\n /** Default true: `useWhyRerender` and the console output assume the component decides on props alone. */\n memoized?: boolean;\n selfDuration?: number;\n treeDuration?: number;\n commitId?: number;\n commitPriority?: CommitPriority;\n source?: SourceLocation;\n updaters?: string[];\n commitCause?: CommitCause;\n afterCommit?: number;\n key?: string | null;\n}\n\n/** `useCart › useState#0` when custom hook names are known, else `useState #0`. */\nexport const hookLabel = (c: { hook: string; index: number; custom?: string[] }): string =>\n c.custom && c.custom.length ? `${c.custom.join(' › ')} › ${c.hook}#${c.index}` : `${c.hook} #${c.index}`;\n\n/** Store-specific advice for a `useSyncExternalStore` snapshot that is a new reference with equal contents. */\nexport function storeAdvice(custom: string[] | undefined): string {\n const chain = custom ?? [];\n if (chain.some((n) => /^useSelector$/.test(n)))\n return 'the Redux selector returns a new object on every call: return a stored slice, pass shallowEqual as the equality function, or memoize it with createSelector';\n if (chain.some((n) => /^useAppSelector$/.test(n)))\n return 'the selector returns a new object on every call: return a stored slice, pass shallowEqual, or memoize it with createSelector';\n if (chain.some((n) => /^use[A-Z]\\w*Store$/.test(n) || n === 'useStore' || n === 'useBoundStore'))\n return 'the store selector returns a new object on every call: select a primitive, or wrap the selector with useShallow (Zustand) / an equality function';\n return 'getSnapshot returns a new reference with the same contents: cache the snapshot in the store and return the same object while the data is unchanged';\n}\n\nconst isGenuine = (c: Change): boolean => c.kind === 'different' || c.kind === 'added' || c.kind === 'removed';\n\n/** `children` re-created in the parent's render: the classic memo-defeater, with its own advice. */\nconst isChildren = (change: Change): boolean => change.path === 'children';\n\nexport function fixFor(change: Change): string {\n if (isChildren(change) && (change.kind === 'element' || change.kind === 'deep-equal')) {\n return `lift the children out of the parent's render: memoize them with useMemo, hoist static elements to module scope, or render them from a component that does not re-render`;\n }\n switch (change.kind) {\n case 'function':\n return `wrap it in useCallback (or hoist it out of the parent's render)`;\n case 'element':\n return `memoize the element with useMemo, or pass it as children from a stable parent`;\n case 'deep-equal':\n return Array.isArray(change.next)\n ? `memoize the array with useMemo (same items, new reference)`\n : `memoize the object with useMemo, or hoist it to module scope if it is constant`;\n default:\n return '';\n }\n}\n\nfunction describe(change: Change): string {\n if (isChildren(change) && (change.kind === 'element' || change.kind === 'deep-equal')) {\n return `children are new React elements with the same types and props on every render of the parent`;\n }\n switch (change.kind) {\n case 'deep-equal':\n return `prop \"${change.path}\" is a new reference but deep-equal to the previous value`;\n case 'function':\n return `prop \"${change.path}\" is a new function instance on every render`;\n case 'element':\n return `prop \"${change.path}\" is a new React element with the same type and props`;\n case 'different':\n return `prop \"${change.path}\" changed`;\n case 'added':\n return `prop \"${change.path}\" was added`;\n case 'removed':\n return `prop \"${change.path}\" was removed`;\n }\n}\n\nexport function buildReport(input: BuildInput): RenderReport {\n const stateChanges = input.stateChanges ?? [];\n const hookChanges = input.hookChanges ?? [];\n const isStateHook = (h: HookChange): boolean => h.hook === 'useState' || h.hook === 'useReducer';\n const genuineProps = input.propChanges.some(isGenuine);\n const genuineState = stateChanges.some(isGenuine) || hookChanges.some((h) => isStateHook(h) && isGenuine(h));\n const genuineHooks = hookChanges.some((h) => !isStateHook(h) && isGenuine(h));\n const causes = [genuineProps && 'props', genuineState && 'state', genuineHooks && 'hooks'].filter(Boolean) as RenderTrigger[];\n\n let trigger: RenderTrigger;\n if (causes.length === 0) trigger = 'parent';\n else if (causes.length === 1) trigger = causes[0]!;\n else trigger = 'mixed';\n\n const avoidable = trigger === 'parent';\n const reasons: string[] = [];\n\n if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {\n const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : 'its parent re-rendered';\n reasons.push(`re-rendered with identical props because ${who}. Wrap \"${input.component}\" in React.memo (or extend PureComponent).`);\n } else if (avoidable && input.parent) {\n reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);\n }\n for (const c of input.propChanges) {\n const fix = fixFor(c);\n reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);\n }\n const memoized = input.memoized !== false;\n if (avoidable && !memoized && input.propChanges.length > 0) {\n reasons.push(\n `\"${input.component}\" is not memoized, so fixing the props alone will not stop this re-render: also wrap it in React.memo (or extend PureComponent).`,\n );\n }\n for (const c of stateChanges) {\n if (isGenuine(c)) reasons.push(`state \"${c.path}\" changed.`);\n else reasons.push(`setState was called with a value deep-equal to the current \"${c.path}\" (new reference, same contents).`);\n }\n for (const c of hookChanges) {\n if (c.hook === 'useContext' && isGenuine(c)) {\n const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : '';\n const keys =\n c.changedKeys && typeof c.totalKeys === 'number' && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys\n ? `: only ${c.changedKeys.map((k) => `\"${k}\"`).join(', ')} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read`\n : '';\n reasons.push(`${c.path} changed${where}${keys}.`);\n } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);\n else if (isStateHook(c))\n reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);\n else if (c.hook === 'useSyncExternalStore') reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: ${storeAdvice(c.custom)}.`);\n else reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: memoize the context/store value where it is produced.`);\n }\n if (input.commitCause === 'effect-after-commit') {\n const who = input.updaters && input.updaters.length ? input.updaters.map((u) => `<${u}>`).join(', ') : 'a component that rendered in it';\n reasons.push(`this commit was scheduled right after commit #${input.afterCommit ?? '?'} by ${who}: an effect there set state (effect → setState loop). Derive the value during render or compute it before setting state.`);\n } else if (input.commitCause === 'suspense-resolved') {\n reasons.push('this commit shows content that was suspended (a Suspense boundary resolved).');\n }\n\n const report: RenderReport = {\n component: input.component,\n instanceId: input.instanceId ?? 0,\n commitId: input.commitId ?? 0,\n renderCount: input.renderCount,\n trigger,\n avoidable,\n memoized,\n props: { prev: input.prevProps, next: input.nextProps },\n propChanges: input.propChanges,\n stateChanges,\n hookChanges,\n parent: input.parent ?? null,\n owner: input.owner ?? null,\n path: input.path ?? [],\n reasons,\n time: now(),\n };\n if (input.selfDuration !== undefined) report.selfDuration = input.selfDuration;\n if (input.treeDuration !== undefined) report.treeDuration = input.treeDuration;\n if (input.commitPriority) report.commitPriority = input.commitPriority;\n if (input.source) report.source = input.source;\n if (input.hookState) report.hookState = input.hookState;\n if (input.contexts) report.contexts = input.contexts;\n if (input.state) report.state = input.state;\n if (input.updaters && input.updaters.length) report.updaters = input.updaters;\n if (input.commitCause) report.commitCause = input.commitCause;\n if (input.afterCommit !== undefined) report.afterCommit = input.afterCommit;\n if (input.key !== undefined) report.key = input.key;\n return report;\n}\n\nfunction describeTrigger(t: RenderTrigger): string {\n switch (t) {\n case 'props':\n return 'its props changed';\n case 'state':\n return 'its state changed';\n case 'hooks':\n return 'a context or store it reads changed';\n case 'mixed':\n return 'its props and state changed';\n default:\n return 'its own parent re-rendered';\n }\n}\n\nconst KIND_LABEL: Record<Change['kind'], string> = {\n 'deep-equal': 'equal by value',\n function: 'new function',\n element: 'equal element',\n different: 'changed',\n added: 'added',\n removed: 'removed',\n};\n\nexport function summarize(report: RenderReport): string {\n const counts = new Map<string, number>();\n for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {\n counts.set(c.kind, (counts.get(c.kind) ?? 0) + 1);\n }\n const parts = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind as Change['kind']]}`);\n if (parts.length === 0) parts.push('no changes');\n return parts.join(', ');\n}\n\n/** Default notifier: prints a console group per report. */\nexport function printReport(report: RenderReport, options: Options): void {\n const c = options.console ?? console;\n const open = options.collapse === false ? c.group : c.groupCollapsed;\n const verdict = report.avoidable ? 'avoidable re-render' : `re-render (${report.trigger})`;\n open.call(c, `[rerender-lens] <${report.component}> ${verdict}: ${summarize(report)}`);\n for (const r of report.reasons) c.log(`- ${r}`);\n if (report.path.length) c.log(`at ${[...report.path, report.component].join(' > ')}`);\n for (const ch of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {\n c.log(`${ch.path} (${KIND_LABEL[ch.kind]})`, { prev: ch.prev, next: ch.next });\n }\n c.log('props', report.props);\n if (report.hookState && report.hookState.length) c.log('hooks', Object.fromEntries(report.hookState.map((h) => [h.path, h.value])));\n if (report.contexts && report.contexts.length) c.log('contexts', Object.fromEntries(report.contexts.map((x) => [x.name, x.value])));\n if (report.state) c.log('state', report.state);\n c.groupEnd();\n}\n","/**\n * Concrete fixes derived from reports, ranked by how many avoidable re-renders each one removes.\n * The same logic drives the extension's Fixes view; here it is typed on `RenderReport` for tests,\n * CI budgets and the CLI. Pure.\n */\nimport type { Change, HookChange, RenderReport } from './types';\nimport { storeAdvice } from './report';\n\nexport type FixKind = 'memo' | 'useCallback' | 'useMemo' | 'useMemoElement' | 'children' | 'contextValue' | 'splitContext' | 'storeSnapshot' | 'bailout';\n\nexport interface Fix {\n kind: FixKind;\n /** Where the change goes (the component that must be edited, or `<Ctx>.Provider`). */\n owner: string;\n /** The component whose re-render this removes. */\n target: string;\n prop: string | null;\n label: string;\n detail: string;\n}\n\nexport interface RankedFix extends Fix {\n key: string;\n /** Avoidable re-renders this fix removes. */\n count: number;\n components: Record<string, number>;\n}\n\nconst AVOIDABLE = new Set<string>(['deep-equal', 'function', 'element']);\nconst rootOf = (path: string): string => path.split(/[.[]/)[0] || path;\n\nexport function fixesFor(r: RenderReport): Fix[] {\n const out: Fix[] = [];\n const owner = r.owner || (r.parent && r.parent.name) || '?';\n const changes: Change[] = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];\n if (r.avoidable && (changes.length === 0 || r.memoized === false)) {\n out.push({\n kind: 'memo',\n owner: r.component,\n target: r.component,\n prop: null,\n label: `Wrap <${r.component}> in React.memo`,\n detail: changes.length === 0 ? `<${r.component}> re-rendered with identical props because <${(r.parent && r.parent.name) || 'its parent'}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`,\n });\n }\n for (const c of r.propChanges) {\n if (!AVOIDABLE.has(c.kind)) continue;\n const root = rootOf(c.path);\n if (root === 'children' && (c.kind === 'element' || c.kind === 'deep-equal')) {\n out.push({ kind: 'children', owner, target: r.component, prop: 'children', label: `memoize children of <${r.component}> in <${owner}>`, detail: `<${owner}> re-creates the children of <${r.component}> on every render.` });\n } else if (c.kind === 'function') {\n out.push({ kind: 'useCallback', owner, target: r.component, prop: root, label: `useCallback(${root}) in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new function on every render of <${owner}>.` });\n } else if (c.kind === 'element') {\n out.push({ kind: 'useMemoElement', owner, target: r.component, prop: root, label: `memoize element prop ${root} in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.` });\n } else {\n out.push({ kind: 'useMemo', owner, target: r.component, prop: root, label: `useMemo(${root}) in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new ${Array.isArray(c.next) ? 'array' : 'object'} with the same contents on every render of <${owner}>.` });\n }\n }\n for (const c of [...r.stateChanges, ...r.hookChanges] as (Change & Partial<HookChange>)[]) {\n const isContext = c.hook === 'useContext';\n const ctxName = isContext ? (/useContext\\((.*)\\)/.exec(c.path) || [])[1] || 'Context' : '';\n const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;\n if (isContext && c.kind === 'different' && c.changedKeys && typeof c.totalKeys === 'number' && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {\n out.push({\n kind: 'splitContext',\n owner: providerOwner || `${ctxName}.Provider`,\n target: r.component,\n prop: ctxName,\n label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ''}: only ${c.changedKeys.join(', ')} changed`,\n detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`,\n });\n continue;\n }\n if (!AVOIDABLE.has(c.kind)) continue;\n if (isContext) {\n out.push({ kind: 'contextValue', owner: providerOwner || `${ctxName}.Provider`, target: r.component, prop: ctxName, label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ''}`, detail: `${ctxName} produced a new value that is deep-equal to the previous one.` });\n } else if (c.hook === 'useSyncExternalStore') {\n const chain = c.custom || [];\n const redux = chain.some((n) => /^use(App)?Selector$/.test(n));\n const zustand = !redux && chain.some((n) => /^use[A-Z]\\w*Store$/.test(n) || n === 'useStore' || n === 'useBoundStore');\n out.push({ kind: 'storeSnapshot', owner: r.component, target: r.component, prop: c.path, label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`, detail: storeAdvice(c.custom) });\n } else {\n out.push({ kind: 'bailout', owner: r.component, target: r.component, prop: c.path, label: `bail out before setting ${c.path} in <${r.component}>`, detail: `${c.path} was set to a value deep-equal to the current one (new reference).` });\n }\n }\n return out;\n}\n\nexport const fixKey = (f: Fix): string => `${f.kind}|${f.owner}|${f.prop || f.target}`;\n\n/** Every fix across `reports`, most re-renders removed first. */\nexport function rankFixes(reports: RenderReport[]): RankedFix[] {\n const byKey = new Map<string, RankedFix>();\n for (const r of reports) {\n if (!r.avoidable) continue;\n for (const f of fixesFor(r)) {\n const key = fixKey(f);\n let agg = byKey.get(key);\n if (!agg) {\n agg = { ...f, key, count: 0, components: {} };\n byKey.set(key, agg);\n }\n agg.count++;\n agg.components[r.component] = (agg.components[r.component] || 0) + 1;\n }\n }\n return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));\n}\n\n/** Multi-line text for a test failure or a CI log. */\nexport function formatFixes(reports: RenderReport[], limit = 10): string {\n const ranked = rankFixes(reports);\n if (!ranked.length) return 'No avoidable re-renders.';\n const lines = ranked.slice(0, limit).map((f, i) => `${String(i + 1).padStart(2)}. ${f.label} (removes ${f.count}: ${Object.entries(f.components).map(([c, n]) => `<${c}>${n > 1 ? ' x' + n : ''}`).join(', ')})`);\n if (ranked.length > limit) lines.push(` … ${ranked.length - limit} more`);\n return lines.join('\\n');\n}\n","/**\n * Re-render budgets for CI: a JSON baseline of allowed avoidable re-renders per component, checked\n * against a collector's reports (like a bundle-size budget). Pure; reading and writing the file is\n * up to the test runner (`fs`), so this works in Node and in the browser.\n */\nimport type { RenderReport } from './types';\nimport { formatFixes } from './fixes';\n\n/** `{ \"*\": 0, \"ProductRow\": 3 }`: max avoidable re-renders per component; `*` is the default. */\nexport type Budget = Record<string, number>;\n\nexport interface BudgetViolation {\n component: string;\n avoidable: number;\n allowed: number;\n}\n\nexport interface BudgetResult {\n ok: boolean;\n violations: BudgetViolation[];\n /** Components with headroom left, so budgets can be tightened. */\n slack: { component: string; avoidable: number; allowed: number }[];\n counts: Record<string, number>;\n}\n\nexport function avoidableCounts(reports: RenderReport[]): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const r of reports) if (r.avoidable) counts[r.component] = (counts[r.component] || 0) + 1;\n return counts;\n}\n\nexport function checkBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n const b: Budget = typeof budget === 'number' ? { '*': budget } : budget;\n const fallback = typeof b['*'] === 'number' ? b['*'] : 0;\n const counts = avoidableCounts(reports);\n const violations: BudgetViolation[] = [];\n const slack: BudgetResult['slack'] = [];\n const names = new Set([...Object.keys(counts), ...Object.keys(b).filter((k) => k !== '*')]);\n for (const component of names) {\n const allowed = typeof b[component] === 'number' ? b[component]! : fallback;\n const avoidable = counts[component] || 0;\n if (avoidable > allowed) violations.push({ component, avoidable, allowed });\n else if (avoidable < allowed) slack.push({ component, avoidable, allowed });\n }\n violations.sort((x, y) => y.avoidable - y.allowed - (x.avoidable - x.allowed));\n return { ok: violations.length === 0, violations, slack, counts };\n}\n\n/** A budget that exactly matches the current counts (the baseline to commit). */\nexport function toBudget(reports: RenderReport[]): Budget {\n return { '*': 0, ...avoidableCounts(reports) };\n}\n\n/** Throws with the violations and the ranked fixes; use it from a test. */\nexport function assertWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n const result = checkBudget(reports, budget);\n if (result.ok) return result;\n const lines = result.violations.map((v) => ` <${v.component}>: ${v.avoidable} avoidable re-render${v.avoidable === 1 ? '' : 's'} (budget ${v.allowed})`);\n throw new Error(`Re-render budget exceeded:\\n${lines.join('\\n')}\\n\\nFixes, most impact first:\\n${formatFixes(reports)}`);\n}\n","/**\n * Playwright helper: run rerender-lens inside any page under test, then pull the reports and assert\n * a budget from the test. Works on production-like builds too (names may be minified there).\n *\n * import { installRerenderLens, pullReports, expectWithinBudget } from 'rerender-lens/playwright';\n * await installRerenderLens(page, { trackAllMemoized: true }); // before page.goto\n * await page.goto('/');\n * ...interact...\n * expectWithinBudget(await pullReports(page), { '*': 0, ProductRow: 3 });\n *\n * The library bundle is injected with `page.addInitScript` at document start, exactly like the\n * extension's injection mode, so `init` runs before react-dom.\n */\nimport { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { RenderReport } from './types';\nimport type { SerializableOptions } from './devtools';\nimport { assertWithinBudget, checkBudget, type Budget, type BudgetResult } from './budget';\nimport { rankFixes, formatFixes, type RankedFix } from './fixes';\n\n/** The subset of Playwright's `Page` used here (keeps `@playwright/test` an optional peer). */\nexport interface PageLike {\n addInitScript(script: string | { content?: string }): Promise<void>;\n evaluate<R>(fn: string | ((arg: unknown) => R), arg?: unknown): Promise<R>;\n}\n\nexport interface InstallOptions extends SerializableOptions {\n /** Forward to a relay (`npx rerender-lens panel`) to watch the test in the panel. */\n relay?: string;\n /** Print to the page console (default false in tests). */\n silent?: boolean;\n /** Library source to inject instead of the packaged IIFE bundle (must define `window.RerenderLens`). */\n bundle?: string;\n}\n\nlet bundleCache: string | null = null;\n\n/** The IIFE bundle (`window.RerenderLens`) the extension injects; shipped as `dist/rerender-lens.iife.js`. */\nexport function libraryBundle(): string {\n if (bundleCache) return bundleCache;\n const here = dirname(fileURLToPath(import.meta.url));\n for (const file of [join(here, 'rerender-lens.iife.js'), join(here, '..', 'extension', 'vendor', 'rerender-lens.js')]) {\n try {\n bundleCache = readFileSync(file, 'utf8');\n return bundleCache;\n } catch {\n /* next */\n }\n }\n throw new Error('rerender-lens: library bundle not found (run `npm run build` in the rerender-lens repo, or reinstall the package)');\n}\n\n/** Script that starts the library with `options` before any page script runs. */\nexport function installScript(options: InstallOptions = {}): string {\n const { relay, bundle, ...rest } = options;\n const opts: InstallOptions = { trackAllMemoized: true, silent: true, ...rest };\n return `${bundle ?? libraryBundle()}\\n;(function(){\\n if (window.__RERENDER_LENS_DEVTOOLS__) return;\\n var L = window.RerenderLens;\\n L.ensureDevtoolsHook();\\n var o = ${JSON.stringify(opts)};\\n o.notifier = L.createDevtoolsNotifier(${relay ? JSON.stringify({ relay }) : '{}'});\\n if (o.include) o.include = o.include.map(function (m) { var r = /^\\\\/(.+)\\\\/([a-z]*)$/.exec(m); return r ? new RegExp(r[1], r[2]) : m; });\\n if (o.exclude) o.exclude = o.exclude.map(function (m) { var r = /^\\\\/(.+)\\\\/([a-z]*)$/.exec(m); return r ? new RegExp(r[1], r[2]) : m; });\\n L.init(o);\\n window.__RERENDER_LENS_PLAYWRIGHT__ = true;\\n})();`;\n}\n\n/** Call before `page.goto`. */\nexport async function installRerenderLens(page: PageLike, options: InstallOptions = {}): Promise<void> {\n await page.addInitScript({ content: installScript(options) });\n}\n\n/** Every report buffered in the page so far (serialized: functions are `ƒ name`, elements `{ $type: 'element' }`). */\nexport async function pullReports(page: PageLike): Promise<RenderReport[]> {\n const result = await page.evaluate<{ reports: unknown[] } | null>('(function(){var b=window.__RERENDER_LENS_DEVTOOLS__;return b&&b.pull?b.pull(0):null})()');\n if (!result) throw new Error('rerender-lens is not running in the page: call installRerenderLens(page) before page.goto');\n return result.reports as RenderReport[];\n}\n\n/** Drop the page buffer (between scenarios). */\nexport async function clearReports(page: PageLike): Promise<void> {\n await page.evaluate('(function(){var b=window.__RERENDER_LENS_DEVTOOLS__;if(b)b.clear();})()');\n}\n\nexport { checkBudget, rankFixes, formatFixes };\nexport type { Budget, BudgetResult, RankedFix };\n\n/** Throws with the violations and ranked fixes, like `collector.assertWithinBudget`. */\nexport function expectWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n return assertWithinBudget(reports, budget);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/report.ts","../src/fixes.ts","../src/budget.ts","../src/playwright.ts"],"names":[],"mappings":"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","file":"playwright.js","sourcesContent":["import type { Change, CommitCause, CommitPriority, HookChange, HookSnapshot, Options, ParentInfo, RenderReport, RenderTrigger, SourceLocation } from './types';\n\nconst now = (): number =>\n typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();\n\nexport interface BuildInput {\n component: string;\n instanceId?: number;\n renderCount: number;\n prevProps: Record<string, unknown>;\n nextProps: Record<string, unknown>;\n propChanges: Change[];\n stateChanges?: Change[];\n hookChanges?: HookChange[];\n hookState?: HookSnapshot[];\n contexts?: { name: string; value: unknown }[];\n state?: Record<string, unknown>;\n parent?: ParentInfo | null;\n owner?: string | null;\n path?: string[];\n /** Default true: `useWhyRerender` and the console output assume the component decides on props alone. */\n memoized?: boolean;\n selfDuration?: number;\n treeDuration?: number;\n commitId?: number;\n commitPriority?: CommitPriority;\n source?: SourceLocation;\n updaters?: string[];\n commitCause?: CommitCause;\n afterCommit?: number;\n key?: string | null;\n}\n\n/** `useCart › useState#0` when custom hook names are known, else `useState #0`. */\nexport const hookLabel = (c: { hook: string; index: number; custom?: string[] }): string =>\n c.custom && c.custom.length ? `${c.custom.join(' › ')} › ${c.hook}#${c.index}` : `${c.hook} #${c.index}`;\n\n/** Store-specific advice for a `useSyncExternalStore` snapshot that is a new reference with equal contents. */\nexport function storeAdvice(custom: string[] | undefined): string {\n const chain = custom ?? [];\n if (chain.some((n) => /^useSelector$/.test(n)))\n return 'the Redux selector returns a new object on every call: return a stored slice, pass shallowEqual as the equality function, or memoize it with createSelector';\n if (chain.some((n) => /^useAppSelector$/.test(n)))\n return 'the selector returns a new object on every call: return a stored slice, pass shallowEqual, or memoize it with createSelector';\n if (chain.some((n) => /^use[A-Z]\\w*Store$/.test(n) || n === 'useStore' || n === 'useBoundStore'))\n return 'the store selector returns a new object on every call: select a primitive, or wrap the selector with useShallow (Zustand) / an equality function';\n return 'getSnapshot returns a new reference with the same contents: cache the snapshot in the store and return the same object while the data is unchanged';\n}\n\nconst isGenuine = (c: Change): boolean => c.kind === 'different' || c.kind === 'added' || c.kind === 'removed';\n\n/** `children` re-created in the parent's render: the classic memo-defeater, with its own advice. */\nconst isChildren = (change: Change): boolean => change.path === 'children';\n\nexport function fixFor(change: Change): string {\n if (isChildren(change) && (change.kind === 'element' || change.kind === 'deep-equal')) {\n return `lift the children out of the parent's render: memoize them with useMemo, hoist static elements to module scope, or render them from a component that does not re-render`;\n }\n switch (change.kind) {\n case 'function':\n return `wrap it in useCallback (or hoist it out of the parent's render)`;\n case 'element':\n return `memoize the element with useMemo, or pass it as children from a stable parent`;\n case 'deep-equal':\n return Array.isArray(change.next)\n ? `memoize the array with useMemo (same items, new reference)`\n : `memoize the object with useMemo, or hoist it to module scope if it is constant`;\n default:\n return '';\n }\n}\n\nfunction describe(change: Change): string {\n if (isChildren(change) && (change.kind === 'element' || change.kind === 'deep-equal')) {\n return `children are new React elements with the same types and props on every render of the parent`;\n }\n switch (change.kind) {\n case 'deep-equal':\n return `prop \"${change.path}\" is a new reference but deep-equal to the previous value`;\n case 'function':\n return `prop \"${change.path}\" is a new function instance on every render`;\n case 'element':\n return `prop \"${change.path}\" is a new React element with the same type and props`;\n case 'different':\n return `prop \"${change.path}\" changed`;\n case 'added':\n return `prop \"${change.path}\" was added`;\n case 'removed':\n return `prop \"${change.path}\" was removed`;\n }\n}\n\nexport function buildReport(input: BuildInput): RenderReport {\n const stateChanges = input.stateChanges ?? [];\n const hookChanges = input.hookChanges ?? [];\n const isStateHook = (h: HookChange): boolean => h.hook === 'useState' || h.hook === 'useReducer';\n const genuineProps = input.propChanges.some(isGenuine);\n const genuineState = stateChanges.some(isGenuine) || hookChanges.some((h) => isStateHook(h) && isGenuine(h));\n const genuineHooks = hookChanges.some((h) => !isStateHook(h) && isGenuine(h));\n const causes = [genuineProps && 'props', genuineState && 'state', genuineHooks && 'hooks'].filter(Boolean) as RenderTrigger[];\n\n let trigger: RenderTrigger;\n if (causes.length === 0) trigger = 'parent';\n else if (causes.length === 1) trigger = causes[0]!;\n else trigger = 'mixed';\n\n const avoidable = trigger === 'parent';\n const reasons: string[] = [];\n\n if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {\n const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : 'its parent re-rendered';\n reasons.push(`re-rendered with identical props because ${who}. Wrap \"${input.component}\" in React.memo (or extend PureComponent).`);\n } else if (avoidable && input.parent) {\n reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);\n }\n for (const c of input.propChanges) {\n const fix = fixFor(c);\n reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);\n }\n const memoized = input.memoized !== false;\n if (avoidable && !memoized && input.propChanges.length > 0) {\n reasons.push(\n `\"${input.component}\" is not memoized, so fixing the props alone will not stop this re-render: also wrap it in React.memo (or extend PureComponent).`,\n );\n }\n for (const c of stateChanges) {\n if (isGenuine(c)) reasons.push(`state \"${c.path}\" changed.`);\n else reasons.push(`setState was called with a value deep-equal to the current \"${c.path}\" (new reference, same contents).`);\n }\n for (const c of hookChanges) {\n if (c.hook === 'useContext' && isGenuine(c)) {\n const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : '';\n const keys =\n c.changedKeys && typeof c.totalKeys === 'number' && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys\n ? `: only ${c.changedKeys.map((k) => `\"${k}\"`).join(', ')} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read`\n : '';\n reasons.push(`${c.path} changed${where}${keys}.`);\n } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);\n else if (isStateHook(c))\n reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);\n else if (c.hook === 'useSyncExternalStore') reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: ${storeAdvice(c.custom)}.`);\n else reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: memoize the context/store value where it is produced.`);\n }\n if (input.commitCause === 'effect-after-commit') {\n const who = input.updaters && input.updaters.length ? input.updaters.map((u) => `<${u}>`).join(', ') : 'a component that rendered in it';\n reasons.push(`this commit was scheduled right after commit #${input.afterCommit ?? '?'} by ${who}: an effect there set state (effect → setState loop). Derive the value during render or compute it before setting state.`);\n } else if (input.commitCause === 'suspense-resolved') {\n reasons.push('this commit shows content that was suspended (a Suspense boundary resolved).');\n }\n\n const report: RenderReport = {\n component: input.component,\n instanceId: input.instanceId ?? 0,\n commitId: input.commitId ?? 0,\n renderCount: input.renderCount,\n trigger,\n avoidable,\n memoized,\n props: { prev: input.prevProps, next: input.nextProps },\n propChanges: input.propChanges,\n stateChanges,\n hookChanges,\n parent: input.parent ?? null,\n owner: input.owner ?? null,\n path: input.path ?? [],\n reasons,\n time: now(),\n };\n if (input.selfDuration !== undefined) report.selfDuration = input.selfDuration;\n if (input.treeDuration !== undefined) report.treeDuration = input.treeDuration;\n if (input.commitPriority) report.commitPriority = input.commitPriority;\n if (input.source) report.source = input.source;\n if (input.hookState) report.hookState = input.hookState;\n if (input.contexts) report.contexts = input.contexts;\n if (input.state) report.state = input.state;\n if (input.updaters && input.updaters.length) report.updaters = input.updaters;\n if (input.commitCause) report.commitCause = input.commitCause;\n if (input.afterCommit !== undefined) report.afterCommit = input.afterCommit;\n if (input.key !== undefined) report.key = input.key;\n return report;\n}\n\nfunction describeTrigger(t: RenderTrigger): string {\n switch (t) {\n case 'props':\n return 'its props changed';\n case 'state':\n return 'its state changed';\n case 'hooks':\n return 'a context or store it reads changed';\n case 'mixed':\n return 'its props and state changed';\n default:\n return 'its own parent re-rendered';\n }\n}\n\nconst KIND_LABEL: Record<Change['kind'], string> = {\n 'deep-equal': 'equal by value',\n function: 'new function',\n element: 'equal element',\n different: 'changed',\n added: 'added',\n removed: 'removed',\n};\n\nexport function summarize(report: RenderReport): string {\n const counts = new Map<string, number>();\n for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {\n counts.set(c.kind, (counts.get(c.kind) ?? 0) + 1);\n }\n const parts = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind as Change['kind']]}`);\n if (parts.length === 0) parts.push('no changes');\n return parts.join(', ');\n}\n\n/** Default notifier: prints a console group per report. */\nexport function printReport(report: RenderReport, options: Options): void {\n const c = options.console ?? console;\n const open = options.collapse === false ? c.group : c.groupCollapsed;\n const verdict = report.avoidable ? 'avoidable re-render' : `re-render (${report.trigger})`;\n open.call(c, `[rerender-lens] <${report.component}> ${verdict}: ${summarize(report)}`);\n for (const r of report.reasons) c.log(`- ${r}`);\n if (report.path.length) c.log(`at ${[...report.path, report.component].join(' > ')}`);\n for (const ch of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {\n c.log(`${ch.path} (${KIND_LABEL[ch.kind]})`, { prev: ch.prev, next: ch.next });\n }\n c.log('props', report.props);\n if (report.hookState && report.hookState.length) c.log('hooks', Object.fromEntries(report.hookState.map((h) => [h.path, h.value])));\n if (report.contexts && report.contexts.length) c.log('contexts', Object.fromEntries(report.contexts.map((x) => [x.name, x.value])));\n if (report.state) c.log('state', report.state);\n c.groupEnd();\n}\n","/**\n * Concrete fixes derived from reports, ranked by how many avoidable re-renders each one removes.\n * The same logic drives the extension's Fixes view; here it is typed on `RenderReport` for tests,\n * CI budgets and the CLI. Pure.\n */\nimport type { Change, HookChange, RenderReport } from './types';\nimport { storeAdvice } from './report';\n\nexport type FixKind = 'memo' | 'useCallback' | 'useMemo' | 'useMemoElement' | 'children' | 'contextValue' | 'splitContext' | 'storeSnapshot' | 'bailout';\n\nexport interface Fix {\n kind: FixKind;\n /** Where the change goes (the component that must be edited, or `<Ctx>.Provider`). */\n owner: string;\n /** The component whose re-render this removes. */\n target: string;\n prop: string | null;\n label: string;\n detail: string;\n}\n\nexport interface RankedFix extends Fix {\n key: string;\n /** Avoidable re-renders this fix removes. */\n count: number;\n components: Record<string, number>;\n}\n\nconst AVOIDABLE = new Set<string>(['deep-equal', 'function', 'element']);\nconst rootOf = (path: string): string => path.split(/[.[]/)[0] || path;\n\nexport function fixesFor(r: RenderReport): Fix[] {\n const out: Fix[] = [];\n const owner = r.owner || (r.parent && r.parent.name) || '?';\n const changes: Change[] = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];\n if (r.avoidable && (changes.length === 0 || r.memoized === false)) {\n out.push({\n kind: 'memo',\n owner: r.component,\n target: r.component,\n prop: null,\n label: `Wrap <${r.component}> in React.memo`,\n detail: changes.length === 0 ? `<${r.component}> re-rendered with identical props because <${(r.parent && r.parent.name) || 'its parent'}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`,\n });\n }\n for (const c of r.propChanges) {\n if (!AVOIDABLE.has(c.kind)) continue;\n const root = rootOf(c.path);\n if (root === 'children' && (c.kind === 'element' || c.kind === 'deep-equal')) {\n out.push({ kind: 'children', owner, target: r.component, prop: 'children', label: `memoize children of <${r.component}> in <${owner}>`, detail: `<${owner}> re-creates the children of <${r.component}> on every render.` });\n } else if (c.kind === 'function') {\n out.push({ kind: 'useCallback', owner, target: r.component, prop: root, label: `useCallback(${root}) in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new function on every render of <${owner}>.` });\n } else if (c.kind === 'element') {\n out.push({ kind: 'useMemoElement', owner, target: r.component, prop: root, label: `memoize element prop ${root} in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.` });\n } else {\n out.push({ kind: 'useMemo', owner, target: r.component, prop: root, label: `useMemo(${root}) in <${owner}>`, detail: `prop \"${c.path}\" of <${r.component}> is a new ${Array.isArray(c.next) ? 'array' : 'object'} with the same contents on every render of <${owner}>.` });\n }\n }\n for (const c of [...r.stateChanges, ...r.hookChanges] as (Change & Partial<HookChange>)[]) {\n const isContext = c.hook === 'useContext';\n const ctxName = isContext ? (/useContext\\((.*)\\)/.exec(c.path) || [])[1] || 'Context' : '';\n const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;\n if (isContext && c.kind === 'different' && c.changedKeys && typeof c.totalKeys === 'number' && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {\n out.push({\n kind: 'splitContext',\n owner: providerOwner || `${ctxName}.Provider`,\n target: r.component,\n prop: ctxName,\n label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ''}: only ${c.changedKeys.join(', ')} changed`,\n detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`,\n });\n continue;\n }\n if (!AVOIDABLE.has(c.kind)) continue;\n if (isContext) {\n out.push({ kind: 'contextValue', owner: providerOwner || `${ctxName}.Provider`, target: r.component, prop: ctxName, label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ''}`, detail: `${ctxName} produced a new value that is deep-equal to the previous one.` });\n } else if (c.hook === 'useSyncExternalStore') {\n const chain = c.custom || [];\n const redux = chain.some((n) => /^use(App)?Selector$/.test(n));\n const zustand = !redux && chain.some((n) => /^use[A-Z]\\w*Store$/.test(n) || n === 'useStore' || n === 'useBoundStore');\n out.push({ kind: 'storeSnapshot', owner: r.component, target: r.component, prop: c.path, label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`, detail: storeAdvice(c.custom) });\n } else {\n out.push({ kind: 'bailout', owner: r.component, target: r.component, prop: c.path, label: `bail out before setting ${c.path} in <${r.component}>`, detail: `${c.path} was set to a value deep-equal to the current one (new reference).` });\n }\n }\n return out;\n}\n\nexport const fixKey = (f: Fix): string => `${f.kind}|${f.owner}|${f.prop || f.target}`;\n\n/** Every fix across `reports`, most re-renders removed first. */\nexport function rankFixes(reports: RenderReport[]): RankedFix[] {\n const byKey = new Map<string, RankedFix>();\n for (const r of reports) {\n if (!r.avoidable) continue;\n for (const f of fixesFor(r)) {\n const key = fixKey(f);\n let agg = byKey.get(key);\n if (!agg) {\n agg = { ...f, key, count: 0, components: {} };\n byKey.set(key, agg);\n }\n agg.count++;\n agg.components[r.component] = (agg.components[r.component] || 0) + 1;\n }\n }\n return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));\n}\n\n/** Multi-line text for a test failure or a CI log. */\nexport function formatFixes(reports: RenderReport[], limit = 10): string {\n const ranked = rankFixes(reports);\n if (!ranked.length) return 'No avoidable re-renders.';\n const lines = ranked.slice(0, limit).map((f, i) => `${String(i + 1).padStart(2)}. ${f.label} (removes ${f.count}: ${Object.entries(f.components).map(([c, n]) => `<${c}>${n > 1 ? ' x' + n : ''}`).join(', ')})`);\n if (ranked.length > limit) lines.push(` … ${ranked.length - limit} more`);\n return lines.join('\\n');\n}\n","/**\n * Re-render budgets for CI: a JSON baseline of allowed avoidable re-renders per component, checked\n * against a collector's reports (like a bundle-size budget). Pure; reading and writing the file is\n * up to the test runner (`fs`), so this works in Node and in the browser.\n */\nimport type { RenderReport } from './types';\nimport { formatFixes } from './fixes';\n\n/** `{ \"*\": 0, \"ProductRow\": 3 }`: max avoidable re-renders per component; `*` is the default. */\nexport type Budget = Record<string, number>;\n\nexport interface BudgetViolation {\n component: string;\n avoidable: number;\n allowed: number;\n}\n\nexport interface BudgetResult {\n ok: boolean;\n violations: BudgetViolation[];\n /** Components with headroom left, so budgets can be tightened. */\n slack: { component: string; avoidable: number; allowed: number }[];\n counts: Record<string, number>;\n}\n\nexport function avoidableCounts(reports: RenderReport[]): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const r of reports) if (r.avoidable) counts[r.component] = (counts[r.component] || 0) + 1;\n return counts;\n}\n\nexport function checkBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n const b: Budget = typeof budget === 'number' ? { '*': budget } : budget;\n const fallback = typeof b['*'] === 'number' ? b['*'] : 0;\n const counts = avoidableCounts(reports);\n const violations: BudgetViolation[] = [];\n const slack: BudgetResult['slack'] = [];\n const names = new Set([...Object.keys(counts), ...Object.keys(b).filter((k) => k !== '*')]);\n for (const component of names) {\n const allowed = typeof b[component] === 'number' ? b[component]! : fallback;\n const avoidable = counts[component] || 0;\n if (avoidable > allowed) violations.push({ component, avoidable, allowed });\n else if (avoidable < allowed) slack.push({ component, avoidable, allowed });\n }\n violations.sort((x, y) => y.avoidable - y.allowed - (x.avoidable - x.allowed));\n return { ok: violations.length === 0, violations, slack, counts };\n}\n\n/** A budget that exactly matches the current counts (the baseline to commit). */\nexport function toBudget(reports: RenderReport[]): Budget {\n return { '*': 0, ...avoidableCounts(reports) };\n}\n\n/** Throws with the violations and the ranked fixes; use it from a test. */\nexport function assertWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n const result = checkBudget(reports, budget);\n if (result.ok) return result;\n const lines = result.violations.map((v) => ` <${v.component}>: ${v.avoidable} avoidable re-render${v.avoidable === 1 ? '' : 's'} (budget ${v.allowed})`);\n throw new Error(`Re-render budget exceeded:\\n${lines.join('\\n')}\\n\\nFixes, most impact first:\\n${formatFixes(reports)}`);\n}\n","/**\n * Playwright helper: run rerender-lens inside any page under test, then pull the reports and assert\n * a budget from the test. Works on production-like builds too (names may be minified there).\n *\n * import { installRerenderLens, pullReports, expectWithinBudget } from 'rerender-lens/playwright';\n * await installRerenderLens(page, { trackAllMemoized: true }); // before page.goto\n * await page.goto('/');\n * ...interact...\n * expectWithinBudget(await pullReports(page), { '*': 0, ProductRow: 3 });\n *\n * The library bundle is injected with `page.addInitScript` at document start, exactly like the\n * extension's injection mode, so `init` runs before react-dom.\n */\nimport { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { RenderReport } from './types';\nimport type { SerializableOptions } from './devtools';\nimport { assertWithinBudget, checkBudget, type Budget, type BudgetResult } from './budget';\nimport { rankFixes, formatFixes, type RankedFix } from './fixes';\n\n/** The subset of Playwright's `Page` used here (keeps `@playwright/test` an optional peer). */\nexport interface PageLike {\n addInitScript(script: string | { content?: string }): Promise<void>;\n evaluate<R>(fn: string | ((arg: unknown) => R), arg?: unknown): Promise<R>;\n}\n\nexport interface InstallOptions extends SerializableOptions {\n /** Forward to a relay (`npx rerender-lens panel`) to watch the test in the panel. */\n relay?: string;\n /** Print to the page console (default false in tests). */\n silent?: boolean;\n /** Library source to inject instead of the packaged IIFE bundle (must define `window.RerenderLens`). */\n bundle?: string;\n}\n\nlet bundleCache: string | null = null;\n\n/** The vendor scripts the extension injects, in load order (see scripts/build-vendor.mjs). */\nexport const VENDOR_FILES = ['rerender-lens.core.js', 'rerender-lens.engine.js', 'rerender-lens.js'];\n\n/** The library as one script (`window.RerenderLens`): `dist/rerender-lens.iife.js`, or the vendor parts joined in this repo. */\nexport function libraryBundle(): string {\n if (bundleCache) return bundleCache;\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [[join(here, 'rerender-lens.iife.js')], VENDOR_FILES.map((f) => join(here, '..', 'extension', 'vendor', f))];\n for (const files of candidates) {\n try {\n bundleCache = files.map((f) => readFileSync(f, 'utf8')).join('\\n;\\n');\n return bundleCache;\n } catch {\n /* next */\n }\n }\n throw new Error('rerender-lens: library bundle not found (run `npm run build` in the rerender-lens repo, or reinstall the package)');\n}\n\n/** Script that starts the library with `options` before any page script runs. */\nexport function installScript(options: InstallOptions = {}): string {\n const { relay, bundle, ...rest } = options;\n const opts: InstallOptions = { trackAllMemoized: true, silent: true, ...rest };\n return `${bundle ?? libraryBundle()}\\n;(function(){\\n if (window.__RERENDER_LENS_DEVTOOLS__) return;\\n var L = window.RerenderLens;\\n L.ensureDevtoolsHook();\\n var o = ${JSON.stringify(opts)};\\n o.notifier = L.createDevtoolsNotifier(${relay ? JSON.stringify({ relay }) : '{}'});\\n if (o.include) o.include = o.include.map(function (m) { var r = /^\\\\/(.+)\\\\/([a-z]*)$/.exec(m); return r ? new RegExp(r[1], r[2]) : m; });\\n if (o.exclude) o.exclude = o.exclude.map(function (m) { var r = /^\\\\/(.+)\\\\/([a-z]*)$/.exec(m); return r ? new RegExp(r[1], r[2]) : m; });\\n L.init(o);\\n window.__RERENDER_LENS_PLAYWRIGHT__ = true;\\n})();`;\n}\n\n/** Call before `page.goto`. */\nexport async function installRerenderLens(page: PageLike, options: InstallOptions = {}): Promise<void> {\n await page.addInitScript({ content: installScript(options) });\n}\n\n/** Every report buffered in the page so far (serialized: functions are `ƒ name`, elements `{ $type: 'element' }`). */\nexport async function pullReports(page: PageLike): Promise<RenderReport[]> {\n const result = await page.evaluate<{ reports: unknown[] } | null>('(function(){var b=window.__RERENDER_LENS_DEVTOOLS__;return b&&b.pull?b.pull(0):null})()');\n if (!result) throw new Error('rerender-lens is not running in the page: call installRerenderLens(page) before page.goto');\n return result.reports as RenderReport[];\n}\n\n/** Drop the page buffer (between scenarios). */\nexport async function clearReports(page: PageLike): Promise<void> {\n await page.evaluate('(function(){var b=window.__RERENDER_LENS_DEVTOOLS__;if(b)b.clear();})()');\n}\n\nexport { checkBudget, rankFixes, formatFixes };\nexport type { Budget, BudgetResult, RankedFix };\n\n/** Throws with the violations and ranked fixes, like `collector.assertWithinBudget`. */\nexport function expectWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult {\n return assertWithinBudget(reports, budget);\n}\n"]}
|