@lownoise-studio/render-shield-react 0.3.0-alpha.1

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/README.md ADDED
@@ -0,0 +1,334 @@
1
+ RenderShield React
2
+ A verification layer for React render decisions.
3
+
4
+ React can skip rerenders.
5
+
6
+ But when it does — how do you verify what was actually prevented, and why?
7
+
8
+ RenderShield React is a lightweight developer instrument that:
9
+
10
+ Applies structured prop comparison
11
+
12
+ Allows surgical deep-watching of specific nested paths
13
+
14
+ Reports why a render was shielded or accepted
15
+
16
+ It does not mutate props.
17
+ It does not rewrite state.
18
+ It does not guarantee performance gains.
19
+
20
+ It exposes the decision boundary.
21
+
22
+ It prefers doing nothing over doing the wrong thing.
23
+
24
+ Why This Exists
25
+
26
+ Many unnecessary rerenders originate from:
27
+
28
+ Unstable object references
29
+
30
+ Inline functions recreated each render
31
+
32
+ Deep state updates unrelated to rendered output
33
+
34
+ Parent rerender cascades
35
+
36
+ AI-generated components that leak references
37
+
38
+ Blindly applying React.memo, useMemo, or useCallback can obscure the underlying cause.
39
+
40
+ RenderShield React is not a magic fix.
41
+
42
+ It is a visibility instrument.
43
+
44
+ It helps you answer:
45
+
46
+ Did this render actually need to happen?
47
+
48
+ Which keys changed?
49
+
50
+ Were watched paths stable?
51
+
52
+ Was shielding correct?
53
+
54
+ You don’t guess.
55
+
56
+ You verify.
57
+
58
+ Installation
59
+ npm install @lownoise-studio/render-shield-react
60
+
61
+ Core Hook
62
+ useRenderShield(
63
+ value: T,
64
+ options?: {
65
+ watch?: string[];
66
+ debug?: boolean;
67
+ visual?: boolean;
68
+ customCompare?: (prev: T, next: T) => boolean;
69
+ componentName?: string;
70
+ }
71
+ )
72
+
73
+ Default Behavior (Shallow Comparison)
74
+
75
+ By default, the hook performs a shallow comparison of top-level keys.
76
+
77
+ If no top-level keys changed → previous reference is returned.
78
+
79
+ If any top-level key changed → new value is accepted.
80
+
81
+ No mutation occurs.
82
+
83
+ Original references are preserved.
84
+
85
+ Shallow comparison remains O(n) where n is the number of top-level keys.
86
+
87
+ No hidden recursion.
88
+
89
+ Watch Paths (Targeted Deep Comparison)
90
+
91
+ You may provide specific nested paths to compare:
92
+
93
+ const shieldedProps = useRenderShield(props, {
94
+ watch: ["user.id"]
95
+ });
96
+
97
+
98
+ When watch is provided:
99
+
100
+ Only those paths are deep-compared.
101
+
102
+ No full-object recursion occurs.
103
+
104
+ If watched paths are stable, shielding may occur even if unrelated keys changed.
105
+
106
+ If a watched path changes, shielding is disabled.
107
+
108
+ This keeps comparison surgical and intentional.
109
+
110
+ Custom Comparator
111
+
112
+ You may supply your own comparison logic:
113
+
114
+ useRenderShield(props, {
115
+ customCompare: (prev, next) => prev.id === next.id
116
+ });
117
+
118
+
119
+ If provided:
120
+
121
+ The custom comparator takes precedence.
122
+
123
+ Comparison logic remains explicit and user-defined.
124
+
125
+ No additional heuristics are applied.
126
+
127
+ Debug Diagnostics
128
+
129
+ Enable diagnostic logging:
130
+
131
+ useRenderShield(props, {
132
+ watch: ["user.id"],
133
+ debug: true
134
+ });
135
+
136
+
137
+ Console output includes:
138
+
139
+ Whether shielding occurred
140
+
141
+ Render count
142
+
143
+ Changed keys
144
+
145
+ Stable keys
146
+
147
+ Watched path results
148
+
149
+ Classification severity
150
+
151
+ Logs are disabled in production builds.
152
+
153
+ Debug mode is strictly for development analysis.
154
+
155
+ Optional Visual HUD (v0.3+)
156
+
157
+ You may enable a minimal visual overlay during development:
158
+
159
+ useRenderShield(props, {
160
+ watch: ["user.id"],
161
+ debug: true,
162
+ visual: true
163
+ });
164
+
165
+
166
+ When:
167
+
168
+ debug === true
169
+
170
+ visual === true
171
+
172
+ a render was successfully shielded
173
+
174
+ A small, temporary development HUD toast appears.
175
+
176
+ Design constraints:
177
+
178
+ SSR safe (document guard)
179
+
180
+ No React lifecycle injection
181
+
182
+ Single shared DOM node
183
+
184
+ Auto-removal after ~2 seconds
185
+
186
+ No global state mutation
187
+
188
+ This is a development instrument — not a UI system.
189
+
190
+ Higher-Order Component
191
+ const Shielded = withRenderShield(Component, {
192
+ watch: ["user.id"],
193
+ debug: true
194
+ });
195
+
196
+
197
+ The HOC:
198
+
199
+ Wraps React.memo
200
+
201
+ Applies the same comparison logic
202
+
203
+ Does not mutate props
204
+
205
+ Does not inject state
206
+
207
+ Does not modify component behavior
208
+
209
+ It influences rerender decisions only.
210
+
211
+ Severity Classification
212
+
213
+ When debug mode is enabled, comparisons are classified as:
214
+
215
+ Stable
216
+
217
+ Changed (non-UI key)
218
+
219
+ Changed (watched key)
220
+
221
+ Custom compare triggered
222
+
223
+ These classifications are informational.
224
+
225
+ They do not alter runtime behavior.
226
+
227
+ Example: The Invisible Cascade
228
+
229
+ A parent updates user.lastActive every 800ms.
230
+
231
+ Your component only depends on user.id.
232
+
233
+ Without structured comparison, rerenders may cascade silently.
234
+
235
+ With RenderShield:
236
+
237
+ useRenderShield(props, {
238
+ watch: ["user.id"],
239
+ debug: true
240
+ });
241
+
242
+
243
+ Only user.id is deep-compared.
244
+
245
+ Unrelated changes are classified and reported.
246
+
247
+ You don’t guess.
248
+
249
+ You verify.
250
+
251
+ What It Is Not
252
+
253
+ RenderShield React is not:
254
+
255
+ A compiler
256
+
257
+ A code transformation tool
258
+
259
+ A React internals patch
260
+
261
+ A guaranteed performance fix
262
+
263
+ A global runtime modifier
264
+
265
+ A full deep-equality engine by default
266
+
267
+ It favors clarity over automation.
268
+
269
+ It prefers explicit control over hidden behavior.
270
+
271
+ Design Constraints
272
+
273
+ RenderShield React:
274
+
275
+ Is React 18+ compatible
276
+
277
+ Does not rely on experimental APIs
278
+
279
+ Does not mutate inputs
280
+
281
+ Does not modify React internals
282
+
283
+ Does not introduce global side effects
284
+
285
+ Avoids deep recursion unless explicitly requested
286
+
287
+ Keeps shallow comparison O(n)
288
+
289
+ It prefers doing nothing over doing the wrong thing.
290
+
291
+ Intended Use Cases
292
+
293
+ RenderShield React works best when:
294
+
295
+ Diagnosing rerender cascades
296
+
297
+ Auditing AI-generated components
298
+
299
+ Verifying React.memo effectiveness
300
+
301
+ Validating watch-path stability
302
+
303
+ Building controlled component boundaries
304
+
305
+ Teaching render mechanics to teams
306
+
307
+ It is a diagnostic surface.
308
+
309
+ Not an optimization promise.
310
+
311
+ Status
312
+
313
+ v0.2.x
314
+
315
+ Core hook stable
316
+
317
+ HOC stable
318
+
319
+ Watch-path targeting validated
320
+
321
+ Type-safe
322
+
323
+ Tests passing
324
+
325
+ CJS, ESM, and DTS builds
326
+
327
+ v0.3.x introduces optional visual development HUD support.
328
+
329
+ Future versions may explore extended diagnostics or tooling layers.
330
+ The core remains intentionally conservative.
331
+
332
+ License
333
+
334
+ MIT
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+
3
+ type RenderShieldSeverity = "Stable" | "Changed (non-UI key)" | "Changed (watched key)" | "Custom compare triggered";
4
+ /** @deprecated Use RenderShieldSeverity. Kept for backward compatibility. */
5
+ type DiffSeverity = RenderShieldSeverity;
6
+ type RenderShieldDiff = {
7
+ componentName?: string;
8
+ shielded: boolean;
9
+ renderCount: number;
10
+ changedKeys: string[];
11
+ stableKeys: string[];
12
+ watchedChanged: string[];
13
+ watchedStable: string[];
14
+ severity: RenderShieldSeverity;
15
+ /**
16
+ * v0.3.0
17
+ * Enables the dev HUD toast for this report event.
18
+ * Set by hook/HOC from RenderShieldOptions.visual.
19
+ */
20
+ visual?: boolean;
21
+ };
22
+ type RenderShieldOptions<T> = {
23
+ watch?: string[];
24
+ debug?: boolean;
25
+ /**
26
+ * v0.3.0
27
+ * When true, render shielded events will show a small dev HUD toast.
28
+ */
29
+ visual?: boolean;
30
+ customCompare?: (prev: T, next: T) => boolean;
31
+ /**
32
+ * Optional explicit name for hook-based usage (HOC derives name automatically).
33
+ */
34
+ componentName?: string;
35
+ };
36
+
37
+ /**
38
+ * useRenderShield
39
+ * - Shields by returning the previous value when the selected comparison says "equal".
40
+ * - Emits dev-only diagnostics when options.debug === true
41
+ *
42
+ * v0.3.0:
43
+ * - options.visual enables the dev HUD toast (only when shielded).
44
+ * - visual flag is carried into the report diff, so report.ts can decide.
45
+ */
46
+ declare function useRenderShield<T>(value: T, options?: RenderShieldOptions<T>): T;
47
+
48
+ declare function withRenderShield<P extends object>(Component: React.ComponentType<P>, options?: RenderShieldOptions<P>): React.MemoExoticComponent<React.ComponentType<P>>;
49
+
50
+ declare function getShallowDiff(prev: any, next: any): {
51
+ equal: boolean;
52
+ changedKeys: string[];
53
+ stableKeys: string[];
54
+ };
55
+
56
+ declare function getAtPath(obj: any, path: string): any;
57
+ /**
58
+ * Deep compare ONLY the values at watched paths.
59
+ * This is intentionally targeted: no full-object deep recursion.
60
+ */
61
+ declare function compareWatchedPaths<T>(prev: T, next: T, watch: string[]): {
62
+ watchedChanged: string[];
63
+ watchedStable: string[];
64
+ watchedEqual: boolean;
65
+ };
66
+ /**
67
+ * Minimal deep equality for watched values.
68
+ * - Cycle-safe
69
+ * - Handles primitives, arrays, plain objects
70
+ * - Not intended for huge graphs (watch paths should stay small + intentional)
71
+ */
72
+ declare function deepEqual(a: any, b: any, seen?: WeakMap<object, object>): boolean;
73
+
74
+ export { type DiffSeverity, type RenderShieldDiff, type RenderShieldOptions, compareWatchedPaths, deepEqual, getAtPath, getShallowDiff, useRenderShield, withRenderShield };
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+
3
+ type RenderShieldSeverity = "Stable" | "Changed (non-UI key)" | "Changed (watched key)" | "Custom compare triggered";
4
+ /** @deprecated Use RenderShieldSeverity. Kept for backward compatibility. */
5
+ type DiffSeverity = RenderShieldSeverity;
6
+ type RenderShieldDiff = {
7
+ componentName?: string;
8
+ shielded: boolean;
9
+ renderCount: number;
10
+ changedKeys: string[];
11
+ stableKeys: string[];
12
+ watchedChanged: string[];
13
+ watchedStable: string[];
14
+ severity: RenderShieldSeverity;
15
+ /**
16
+ * v0.3.0
17
+ * Enables the dev HUD toast for this report event.
18
+ * Set by hook/HOC from RenderShieldOptions.visual.
19
+ */
20
+ visual?: boolean;
21
+ };
22
+ type RenderShieldOptions<T> = {
23
+ watch?: string[];
24
+ debug?: boolean;
25
+ /**
26
+ * v0.3.0
27
+ * When true, render shielded events will show a small dev HUD toast.
28
+ */
29
+ visual?: boolean;
30
+ customCompare?: (prev: T, next: T) => boolean;
31
+ /**
32
+ * Optional explicit name for hook-based usage (HOC derives name automatically).
33
+ */
34
+ componentName?: string;
35
+ };
36
+
37
+ /**
38
+ * useRenderShield
39
+ * - Shields by returning the previous value when the selected comparison says "equal".
40
+ * - Emits dev-only diagnostics when options.debug === true
41
+ *
42
+ * v0.3.0:
43
+ * - options.visual enables the dev HUD toast (only when shielded).
44
+ * - visual flag is carried into the report diff, so report.ts can decide.
45
+ */
46
+ declare function useRenderShield<T>(value: T, options?: RenderShieldOptions<T>): T;
47
+
48
+ declare function withRenderShield<P extends object>(Component: React.ComponentType<P>, options?: RenderShieldOptions<P>): React.MemoExoticComponent<React.ComponentType<P>>;
49
+
50
+ declare function getShallowDiff(prev: any, next: any): {
51
+ equal: boolean;
52
+ changedKeys: string[];
53
+ stableKeys: string[];
54
+ };
55
+
56
+ declare function getAtPath(obj: any, path: string): any;
57
+ /**
58
+ * Deep compare ONLY the values at watched paths.
59
+ * This is intentionally targeted: no full-object deep recursion.
60
+ */
61
+ declare function compareWatchedPaths<T>(prev: T, next: T, watch: string[]): {
62
+ watchedChanged: string[];
63
+ watchedStable: string[];
64
+ watchedEqual: boolean;
65
+ };
66
+ /**
67
+ * Minimal deep equality for watched values.
68
+ * - Cycle-safe
69
+ * - Handles primitives, arrays, plain objects
70
+ * - Not intended for huge graphs (watch paths should stay small + intentional)
71
+ */
72
+ declare function deepEqual(a: any, b: any, seen?: WeakMap<object, object>): boolean;
73
+
74
+ export { type DiffSeverity, type RenderShieldDiff, type RenderShieldOptions, compareWatchedPaths, deepEqual, getAtPath, getShallowDiff, useRenderShield, withRenderShield };
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";var A=Object.create;var m=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var I=Object.getPrototypeOf,E=Object.prototype.hasOwnProperty;var P=(e,n)=>{for(var t in n)m(e,t,{get:n[t],enumerable:!0})},x=(e,n,t,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of q(n))!E.call(e,r)&&r!==t&&m(e,r,{get:()=>n[r],enumerable:!(o=T(n,r))||o.enumerable});return e};var W=(e,n,t)=>(t=e!=null?A(I(e)):{},x(n||!e||!e.__esModule?m(t,"default",{value:e,enumerable:!0}):t,e)),$=e=>x(m({},"__esModule",{value:!0}),e);var Y={};P(Y,{compareWatchedPaths:()=>p,deepEqual:()=>y,getAtPath:()=>b,getShallowDiff:()=>f,useRenderShield:()=>j,withRenderShield:()=>D});module.exports=$(Y);var g=require("react");function f(e,n){if(Object.is(e,n))return{equal:!0,changedKeys:[],stableKeys:M(n)};if(!S(e)||!S(n))return{equal:!1,changedKeys:["(value)"],stableKeys:[]};let t=Object.keys(e),o=Object.keys(n),r=new Set([...t,...o]),a=[],l=[];for(let i of r){let s=e[i],d=n[i];Object.is(s,d)?l.push(i):a.push(i)}return{equal:a.length===0,changedKeys:a,stableKeys:l}}function S(e){return e!==null&&typeof e=="object"}function M(e){return S(e)?Object.keys(e):[]}function b(e,n){if(e==null)return;let t=U(n),o=e;for(let r of t){if(o==null)return;o=o[r]}return o}function p(e,n,t){let o=[],r=[];for(let a of t){let l=b(e,a),i=b(n,a);y(l,i)?r.push(a):o.push(a)}return{watchedChanged:o,watchedStable:r,watchedEqual:o.length===0}}function y(e,n,t=new WeakMap){if(Object.is(e,n))return!0;let o=K(e),r=K(n);if(!o||!r)return!1;let a=t.get(e);if(a&&a===n)return!0;if(t.set(e,n),Array.isArray(e)||Array.isArray(n)){if(!Array.isArray(e)||!Array.isArray(n)||e.length!==n.length)return!1;for(let s=0;s<e.length;s++)if(!y(e[s],n[s],t))return!1;return!0}let l=Object.keys(e),i=Object.keys(n);if(l.length!==i.length)return!1;for(let s of l)if(!Object.prototype.hasOwnProperty.call(n,s))return!1;for(let s of l)if(!y(e[s],n[s],t))return!1;return!0}function K(e){return e!==null&&typeof e=="object"}function U(e){let n=[],t=e.replace(/\[(\d+)\]/g,".$1");for(let o of t.split(".").filter(Boolean)){let r=Number(o);n.push(Number.isInteger(r)&&String(r)===o?r:o)}return n}var C=new Map,R=!1;function u(e){if(B())return;let n=F(e);C.set(n,{key:n,diff:e}),R||(R=!0,Promise.resolve().then(z))}function z(){for(let{diff:e}of C.values())V(e);C.clear(),R=!1}function V(e){let n=e.componentName?`<${e.componentName}>`:"<Component>",t=`[RenderShield] ${n}`;console.groupCollapsed(t),console.log("Shielded:",e.shielded),console.log("Render count:",e.renderCount),console.log("Changed:",e.changedKeys),console.log("Stable:",e.stableKeys),(e.watchedChanged.length||e.watchedStable.length)&&(console.log("Watched changed:",e.watchedChanged),console.log("Watched stable:",e.watchedStable)),console.log("Severity:",e.severity),console.groupEnd(),e.shielded&&e.visual===!0&&H(n,e.severity)}function B(){try{return typeof process<"u"&&!!process.env&&process.env.NODE_ENV==="production"}catch{return!1}}function H(e,n){if(typeof document>"u"||typeof window>"u")return;let t="render-shield-toast",o="render-shield-toast",r=document.getElementById(t);r&&r.remove();let a=document.createElement("div");a.id=t,a.className=o,Object.assign(a.style,{position:"fixed",bottom:"20px",right:"20px",background:"rgba(12, 16, 22, 0.92)",color:"white",padding:"12px 14px",borderRadius:"12px",boxShadow:"0 14px 38px rgba(0,0,0,0.38)",fontFamily:'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"',fontSize:"13px",lineHeight:"1.25",fontWeight:"650",zIndex:"2147483647",display:"flex",alignItems:"center",gap:"10px",border:"1px solid rgba(255,255,255,0.12)",backdropFilter:"blur(8px)",transform:"translateY(12px)",opacity:"0",transition:"transform 180ms ease, opacity 180ms ease",pointerEvents:"none",maxWidth:"340px"});let l=N(e),i=N(n);a.innerHTML=`
2
+ <div style="display:flex; align-items:center; gap:10px;">
3
+ <span aria-hidden="true" style="font-size:16px; line-height:1;">\u{1F6E1}\uFE0F</span>
4
+ <div style="display:flex; flex-direction:column; gap:2px;">
5
+ <div style="opacity:0.98">
6
+ <span style="color: #35d07f; font-weight: 800;">${l}</span> shielded
7
+ </div>
8
+ <div style="font-size:11px; font-weight:600; opacity:0.78">
9
+ ${i}
10
+ </div>
11
+ </div>
12
+ </div>
13
+ `,document.body.appendChild(a),requestAnimationFrame(()=>{a.style.transform="translateY(0)",a.style.opacity="1"}),window.setTimeout(()=>{a.style.transform="translateY(8px)",a.style.opacity="0",window.setTimeout(()=>a.remove(),220)},2e3)}function N(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#039;")}function F(e){let n=e.componentName??"Anonymous",t=[e.shielded?"S1":"S0",e.severity,e.changedKeys.join(","),"|W|",e.watchedChanged.join(",")].join(":");return`${n}::${t}`}function j(e,n){let t=n??{},o=(0,g.useRef)(null),r=(0,g.useRef)(0);r.current+=1;let a=o.current,l=(0,g.useMemo)(()=>{if(a===null){let c=e&&typeof e=="object"?Object.keys(e):[],h={componentName:t.componentName,shielded:!1,renderCount:r.current,changedKeys:[],stableKeys:c,watchedChanged:[],watchedStable:t.watch??[],severity:"Stable",visual:!!t.visual};return{shielded:!1,nextValue:e,shouldReportInitial:!!t.debug,initialDiff:h,diff:void 0}}if(typeof t.customCompare=="function"){let c=t.customCompare(a,e),h={componentName:t.componentName,shielded:c,renderCount:r.current,changedKeys:[],stableKeys:[],watchedChanged:[],watchedStable:t.watch??[],severity:"Custom compare triggered",visual:!!t.visual};return{shielded:c,nextValue:c?a:e,shouldReportInitial:!1,initialDiff:void 0,diff:h}}let i=L(a,e),s=i.changedKeys,d=i.stableKeys;if(t.watch&&t.watch.length>0){let c=p(a,e,t.watch),h=c.watchedEqual,k={componentName:t.componentName,shielded:h,renderCount:r.current,changedKeys:s,stableKeys:d,watchedChanged:c.watchedChanged,watchedStable:c.watchedStable,severity:c.watchedChanged.length>0?"Changed (watched key)":s.length>0?"Changed (non-UI key)":"Stable",visual:!!t.visual};return{shielded:h,nextValue:h?a:e,shouldReportInitial:!1,initialDiff:void 0,diff:k}}let w=i.equal,O={componentName:t.componentName,shielded:w,renderCount:r.current,changedKeys:s,stableKeys:d,watchedChanged:[],watchedStable:[],severity:s.length>0?"Changed (non-UI key)":"Stable",visual:!!t.visual};return{shielded:w,nextValue:w?a:e,shouldReportInitial:!1,initialDiff:void 0,diff:O}},[e,t.componentName,t.debug,t.visual,t.customCompare,t.watch?.join("|")]);return a===null?(o.current=e,l.shouldReportInitial&&l.initialDiff&&u(l.initialDiff),e):(t.debug&&l.diff&&u(l.diff),l.shielded?o.current:(o.current=e,e))}function L(e,n){try{return f(e,n)}catch{return{equal:!1,changedKeys:["(unavailable)"],stableKeys:[]}}}var v=W(require("react"));function D(e,n){let t=n??{},o=e.displayName||e.name||"Component",r=v.default.memo(e,(a,l)=>{if(typeof t.customCompare=="function"){let s=t.customCompare(a,l);return t.debug&&u({componentName:o,shielded:s,renderCount:NaN,changedKeys:[],stableKeys:[],watchedChanged:[],watchedStable:[],severity:"Custom compare triggered"}),s}let i=f(a,l);if(t.watch&&t.watch.length>0){let s=p(a,l,t.watch),d=s.watchedEqual;return t.debug&&u({componentName:o,shielded:d,renderCount:NaN,changedKeys:i.changedKeys,stableKeys:i.stableKeys,watchedChanged:s.watchedChanged,watchedStable:s.watchedStable,severity:s.watchedChanged.length>0?"Changed (watched key)":i.changedKeys.length>0?"Changed (non-UI key)":"Stable"}),d}return t.debug&&u({componentName:o,shielded:i.equal,renderCount:NaN,changedKeys:i.changedKeys,stableKeys:i.stableKeys,watchedChanged:[],watchedStable:[],severity:i.changedKeys.length>0?"Changed (non-UI key)":"Stable"}),i.equal});return r.displayName=`withRenderShield(${o})`,r}0&&(module.exports={compareWatchedPaths,deepEqual,getAtPath,getShallowDiff,useRenderShield,withRenderShield});
package/dist/index.mjs ADDED
@@ -0,0 +1,13 @@
1
+ import{useMemo as q,useRef as x}from"react";function f(e,n){if(Object.is(e,n))return{equal:!0,changedKeys:[],stableKeys:j(n)};if(!m(e)||!m(n))return{equal:!1,changedKeys:["(value)"],stableKeys:[]};let t=Object.keys(e),o=Object.keys(n),r=new Set([...t,...o]),a=[],l=[];for(let i of r){let s=e[i],d=n[i];Object.is(s,d)?l.push(i):a.push(i)}return{equal:a.length===0,changedKeys:a,stableKeys:l}}function m(e){return e!==null&&typeof e=="object"}function j(e){return m(e)?Object.keys(e):[]}function b(e,n){if(e==null)return;let t=v(n),o=e;for(let r of t){if(o==null)return;o=o[r]}return o}function p(e,n,t){let o=[],r=[];for(let a of t){let l=b(e,a),i=b(n,a);y(l,i)?r.push(a):o.push(a)}return{watchedChanged:o,watchedStable:r,watchedEqual:o.length===0}}function y(e,n,t=new WeakMap){if(Object.is(e,n))return!0;let o=C(e),r=C(n);if(!o||!r)return!1;let a=t.get(e);if(a&&a===n)return!0;if(t.set(e,n),Array.isArray(e)||Array.isArray(n)){if(!Array.isArray(e)||!Array.isArray(n)||e.length!==n.length)return!1;for(let s=0;s<e.length;s++)if(!y(e[s],n[s],t))return!1;return!0}let l=Object.keys(e),i=Object.keys(n);if(l.length!==i.length)return!1;for(let s of l)if(!Object.prototype.hasOwnProperty.call(n,s))return!1;for(let s of l)if(!y(e[s],n[s],t))return!1;return!0}function C(e){return e!==null&&typeof e=="object"}function v(e){let n=[],t=e.replace(/\[(\d+)\]/g,".$1");for(let o of t.split(".").filter(Boolean)){let r=Number(o);n.push(Number.isInteger(r)&&String(r)===o?r:o)}return n}var w=new Map,S=!1;function u(e){if(k())return;let n=T(e);w.set(n,{key:n,diff:e}),S||(S=!0,Promise.resolve().then(D))}function D(){for(let{diff:e}of w.values())O(e);w.clear(),S=!1}function O(e){let n=e.componentName?`<${e.componentName}>`:"<Component>",t=`[RenderShield] ${n}`;console.groupCollapsed(t),console.log("Shielded:",e.shielded),console.log("Render count:",e.renderCount),console.log("Changed:",e.changedKeys),console.log("Stable:",e.stableKeys),(e.watchedChanged.length||e.watchedStable.length)&&(console.log("Watched changed:",e.watchedChanged),console.log("Watched stable:",e.watchedStable)),console.log("Severity:",e.severity),console.groupEnd(),e.shielded&&e.visual===!0&&A(n,e.severity)}function k(){try{return typeof process<"u"&&!!process.env&&process.env.NODE_ENV==="production"}catch{return!1}}function A(e,n){if(typeof document>"u"||typeof window>"u")return;let t="render-shield-toast",o="render-shield-toast",r=document.getElementById(t);r&&r.remove();let a=document.createElement("div");a.id=t,a.className=o,Object.assign(a.style,{position:"fixed",bottom:"20px",right:"20px",background:"rgba(12, 16, 22, 0.92)",color:"white",padding:"12px 14px",borderRadius:"12px",boxShadow:"0 14px 38px rgba(0,0,0,0.38)",fontFamily:'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"',fontSize:"13px",lineHeight:"1.25",fontWeight:"650",zIndex:"2147483647",display:"flex",alignItems:"center",gap:"10px",border:"1px solid rgba(255,255,255,0.12)",backdropFilter:"blur(8px)",transform:"translateY(12px)",opacity:"0",transition:"transform 180ms ease, opacity 180ms ease",pointerEvents:"none",maxWidth:"340px"});let l=R(e),i=R(n);a.innerHTML=`
2
+ <div style="display:flex; align-items:center; gap:10px;">
3
+ <span aria-hidden="true" style="font-size:16px; line-height:1;">\u{1F6E1}\uFE0F</span>
4
+ <div style="display:flex; flex-direction:column; gap:2px;">
5
+ <div style="opacity:0.98">
6
+ <span style="color: #35d07f; font-weight: 800;">${l}</span> shielded
7
+ </div>
8
+ <div style="font-size:11px; font-weight:600; opacity:0.78">
9
+ ${i}
10
+ </div>
11
+ </div>
12
+ </div>
13
+ `,document.body.appendChild(a),requestAnimationFrame(()=>{a.style.transform="translateY(0)",a.style.opacity="1"}),window.setTimeout(()=>{a.style.transform="translateY(8px)",a.style.opacity="0",window.setTimeout(()=>a.remove(),220)},2e3)}function R(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#039;")}function T(e){let n=e.componentName??"Anonymous",t=[e.shielded?"S1":"S0",e.severity,e.changedKeys.join(","),"|W|",e.watchedChanged.join(",")].join(":");return`${n}::${t}`}function I(e,n){let t=n??{},o=x(null),r=x(0);r.current+=1;let a=o.current,l=q(()=>{if(a===null){let c=e&&typeof e=="object"?Object.keys(e):[],h={componentName:t.componentName,shielded:!1,renderCount:r.current,changedKeys:[],stableKeys:c,watchedChanged:[],watchedStable:t.watch??[],severity:"Stable",visual:!!t.visual};return{shielded:!1,nextValue:e,shouldReportInitial:!!t.debug,initialDiff:h,diff:void 0}}if(typeof t.customCompare=="function"){let c=t.customCompare(a,e),h={componentName:t.componentName,shielded:c,renderCount:r.current,changedKeys:[],stableKeys:[],watchedChanged:[],watchedStable:t.watch??[],severity:"Custom compare triggered",visual:!!t.visual};return{shielded:c,nextValue:c?a:e,shouldReportInitial:!1,initialDiff:void 0,diff:h}}let i=E(a,e),s=i.changedKeys,d=i.stableKeys;if(t.watch&&t.watch.length>0){let c=p(a,e,t.watch),h=c.watchedEqual,N={componentName:t.componentName,shielded:h,renderCount:r.current,changedKeys:s,stableKeys:d,watchedChanged:c.watchedChanged,watchedStable:c.watchedStable,severity:c.watchedChanged.length>0?"Changed (watched key)":s.length>0?"Changed (non-UI key)":"Stable",visual:!!t.visual};return{shielded:h,nextValue:h?a:e,shouldReportInitial:!1,initialDiff:void 0,diff:N}}let g=i.equal,K={componentName:t.componentName,shielded:g,renderCount:r.current,changedKeys:s,stableKeys:d,watchedChanged:[],watchedStable:[],severity:s.length>0?"Changed (non-UI key)":"Stable",visual:!!t.visual};return{shielded:g,nextValue:g?a:e,shouldReportInitial:!1,initialDiff:void 0,diff:K}},[e,t.componentName,t.debug,t.visual,t.customCompare,t.watch?.join("|")]);return a===null?(o.current=e,l.shouldReportInitial&&l.initialDiff&&u(l.initialDiff),e):(t.debug&&l.diff&&u(l.diff),l.shielded?o.current:(o.current=e,e))}function E(e,n){try{return f(e,n)}catch{return{equal:!1,changedKeys:["(unavailable)"],stableKeys:[]}}}import P from"react";function W(e,n){let t=n??{},o=e.displayName||e.name||"Component",r=P.memo(e,(a,l)=>{if(typeof t.customCompare=="function"){let s=t.customCompare(a,l);return t.debug&&u({componentName:o,shielded:s,renderCount:NaN,changedKeys:[],stableKeys:[],watchedChanged:[],watchedStable:[],severity:"Custom compare triggered"}),s}let i=f(a,l);if(t.watch&&t.watch.length>0){let s=p(a,l,t.watch),d=s.watchedEqual;return t.debug&&u({componentName:o,shielded:d,renderCount:NaN,changedKeys:i.changedKeys,stableKeys:i.stableKeys,watchedChanged:s.watchedChanged,watchedStable:s.watchedStable,severity:s.watchedChanged.length>0?"Changed (watched key)":i.changedKeys.length>0?"Changed (non-UI key)":"Stable"}),d}return t.debug&&u({componentName:o,shielded:i.equal,renderCount:NaN,changedKeys:i.changedKeys,stableKeys:i.stableKeys,watchedChanged:[],watchedStable:[],severity:i.changedKeys.length>0?"Changed (non-UI key)":"Stable"}),i.equal});return r.displayName=`withRenderShield(${o})`,r}export{p as compareWatchedPaths,y as deepEqual,b as getAtPath,f as getShallowDiff,I as useRenderShield,W as withRenderShield};
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@lownoise-studio/render-shield-react",
3
+ "version": "0.3.0-alpha.1",
4
+ "description": "Lightweight React render diff shielding with structured diagnostics.",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.mjs",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "npx tsup",
23
+ "dev": "npx tsup --watch",
24
+ "test": "vitest"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=18"
28
+ },
29
+ "devDependencies": {
30
+ "tsup": "^8.0.0",
31
+ "vitest": "^1.6.0",
32
+ "@testing-library/react": "^14.0.0",
33
+ "jsdom": "^24.0.0",
34
+ "react": "^18.0.0",
35
+ "react-dom": "^18.0.0",
36
+ "typescript": "^5.0.0"
37
+ }
38
+ }