@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 +334 -0
- package/dist/index.d.mts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +13 -0
- package/dist/index.mjs +13 -0
- package/package.json +38 -0
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
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}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("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}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
|
+
}
|